@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,817 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateStats = calculateStats;
4
+ exports.buildEffectiveMatrix = buildEffectiveMatrix;
5
+ exports.collectRoleEnums = collectRoleEnums;
6
+ exports.detectSecurityGaps = detectSecurityGaps;
7
+ exports.discoverModules = discoverModules;
8
+ exports.findProjectRoot = findProjectRoot;
9
+ exports.scanPermissions = scanPermissions;
10
+ exports.scanModule = scanModule;
11
+ exports.scanObjects = scanObjects;
12
+ exports.extractDecoratorRoles = extractDecoratorRoles;
13
+ exports.extractSecurityCheckInfo = extractSecurityCheckInfo;
14
+ exports.formatRole = formatRole;
15
+ exports.formatRolesDisplay = formatRolesDisplay;
16
+ exports.generateMarkdownReport = generateMarkdownReport;
17
+ exports.parseEndpointPermissions = parseEndpointPermissions;
18
+ exports.parseFieldPermission = parseFieldPermission;
19
+ exports.parseFilePermissions = parseFilePermissions;
20
+ exports.parseRestrictedDecorator = parseRestrictedDecorator;
21
+ exports.parseRolesDecorator = parseRolesDecorator;
22
+ exports.resolveInheritedFields = resolveInheritedFields;
23
+ const fs_1 = require("fs");
24
+ const path_1 = require("path");
25
+ const ts_morph_1 = require("ts-morph");
26
+ function calculateStats(modules, objects, warnings) {
27
+ const warningsByType = {
28
+ NO_RESTRICTION: 0,
29
+ NO_ROLES: 0,
30
+ NO_SECURITY_CHECK: 0,
31
+ UNRESTRICTED_FIELD: 0,
32
+ UNRESTRICTED_METHOD: 0,
33
+ };
34
+ for (const w of warnings) {
35
+ if (w.type in warningsByType) {
36
+ warningsByType[w.type]++;
37
+ }
38
+ }
39
+ const totalModels = modules.reduce((s, m) => s + m.models.length, 0);
40
+ let totalMethods = 0;
41
+ let methodsWithRoles = 0;
42
+ for (const mod of modules) {
43
+ for (const ctrl of mod.controllers) {
44
+ for (const m of ctrl.methods) {
45
+ totalMethods++;
46
+ if (m.roles.length > 0 || ctrl.classRoles.length > 0)
47
+ methodsWithRoles++;
48
+ }
49
+ }
50
+ for (const res of mod.resolvers) {
51
+ for (const m of res.methods) {
52
+ totalMethods++;
53
+ if (m.roles.length > 0 || res.classRoles.length > 0)
54
+ methodsWithRoles++;
55
+ }
56
+ }
57
+ }
58
+ let modelsWithBothChecks = 0;
59
+ for (const mod of modules) {
60
+ for (const model of mod.models) {
61
+ if (model.classRestriction.length > 0 && model.securityCheck) {
62
+ modelsWithBothChecks++;
63
+ }
64
+ }
65
+ }
66
+ const endpointCoverage = totalMethods > 0 ? Math.round((methodsWithRoles / totalMethods) * 100) : 100;
67
+ const securityCoverage = totalModels > 0 ? Math.round((modelsWithBothChecks / totalModels) * 100) : 100;
68
+ return {
69
+ endpointCoverage,
70
+ securityCoverage,
71
+ totalEndpoints: totalMethods,
72
+ totalModels,
73
+ totalModules: modules.length,
74
+ totalSubObjects: objects.length,
75
+ totalWarnings: warnings.length,
76
+ warningsByType,
77
+ };
78
+ }
79
+ function buildEffectiveMatrix(mod) {
80
+ const allRoles = new Set();
81
+ for (const ctrl of mod.controllers) {
82
+ for (const r of ctrl.classRoles)
83
+ allRoles.add(r);
84
+ for (const m of ctrl.methods) {
85
+ for (const r of m.roles)
86
+ allRoles.add(r);
87
+ }
88
+ }
89
+ for (const res of mod.resolvers) {
90
+ for (const r of res.classRoles)
91
+ allRoles.add(r);
92
+ for (const m of res.methods) {
93
+ for (const r of m.roles)
94
+ allRoles.add(r);
95
+ }
96
+ }
97
+ const result = [];
98
+ for (const role of [...allRoles].sort()) {
99
+ const endpoints = [];
100
+ for (const ctrl of mod.controllers) {
101
+ for (const m of ctrl.methods) {
102
+ const effective = m.roles.length > 0 ? m.roles : ctrl.classRoles;
103
+ if (effective.includes(role)) {
104
+ endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Controller' });
105
+ }
106
+ }
107
+ }
108
+ for (const res of mod.resolvers) {
109
+ for (const m of res.methods) {
110
+ const effective = m.roles.length > 0 ? m.roles : res.classRoles;
111
+ if (effective.includes(role)) {
112
+ endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Resolver' });
113
+ }
114
+ }
115
+ }
116
+ result.push({ endpoints, role });
117
+ }
118
+ return result;
119
+ }
120
+ function collectRoleEnums(project, projectPath) {
121
+ const enums = [];
122
+ const enumPatterns = [
123
+ (0, path_1.join)(projectPath, 'src', 'server', 'common', 'enums'),
124
+ (0, path_1.join)(projectPath, 'src', 'server', 'modules'),
125
+ ];
126
+ for (const dir of enumPatterns) {
127
+ try {
128
+ const enumFiles = project.addSourceFilesAtPaths((0, path_1.join)(dir, '**', '*.enum.ts'));
129
+ for (const sf of enumFiles) {
130
+ for (const enumDecl of sf.getEnums()) {
131
+ const enumName = enumDecl.getName();
132
+ if (enumName.toLowerCase().includes('role')) {
133
+ const values = enumDecl.getMembers().map((m) => ({
134
+ key: m.getName(),
135
+ value: m.getValue()?.toString() || m.getName(),
136
+ }));
137
+ enums.push({
138
+ file: (0, path_1.relative)(projectPath, sf.getFilePath()),
139
+ name: enumName,
140
+ values,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ catch {
147
+ }
148
+ }
149
+ return enums;
150
+ }
151
+ function detectSecurityGaps(modules, objects) {
152
+ const warnings = [];
153
+ for (const mod of modules) {
154
+ for (const model of mod.models) {
155
+ if (model.classRestriction.length === 0) {
156
+ warnings.push({
157
+ details: `Model ${model.className} has no @Restricted class-level restriction`,
158
+ file: model.filePath,
159
+ module: mod.name,
160
+ type: 'NO_RESTRICTION',
161
+ });
162
+ }
163
+ if (!model.securityCheck) {
164
+ warnings.push({
165
+ details: `Model ${model.className} has no securityCheck override`,
166
+ file: model.filePath,
167
+ module: mod.name,
168
+ type: 'NO_SECURITY_CHECK',
169
+ });
170
+ }
171
+ for (const field of model.fields) {
172
+ if (field.roles === '*(none)*') {
173
+ warnings.push({
174
+ details: `Field '${field.name}' has no role restriction`,
175
+ file: model.filePath,
176
+ module: mod.name,
177
+ type: 'UNRESTRICTED_FIELD',
178
+ });
179
+ }
180
+ }
181
+ }
182
+ for (const input of mod.inputs) {
183
+ for (const field of input.fields) {
184
+ if (field.roles === '*(none)*') {
185
+ warnings.push({
186
+ details: `Field '${field.name}' has no role restriction`,
187
+ file: input.filePath,
188
+ module: mod.name,
189
+ type: 'UNRESTRICTED_FIELD',
190
+ });
191
+ }
192
+ }
193
+ }
194
+ for (const ctrl of mod.controllers) {
195
+ if (ctrl.classRoles.length === 0) {
196
+ warnings.push({
197
+ details: `Controller ${ctrl.className} has no @Roles class-level restriction`,
198
+ file: ctrl.filePath,
199
+ module: mod.name,
200
+ type: 'NO_ROLES',
201
+ });
202
+ }
203
+ for (const method of ctrl.methods) {
204
+ if (method.roles.length === 0 && ctrl.classRoles.length === 0) {
205
+ warnings.push({
206
+ details: `Method '${method.name}' has no @Roles and class has no @Roles`,
207
+ file: ctrl.filePath,
208
+ module: mod.name,
209
+ type: 'UNRESTRICTED_METHOD',
210
+ });
211
+ }
212
+ }
213
+ }
214
+ for (const res of mod.resolvers) {
215
+ if (res.classRoles.length === 0) {
216
+ warnings.push({
217
+ details: `Resolver ${res.className} has no @Roles class-level restriction`,
218
+ file: res.filePath,
219
+ module: mod.name,
220
+ type: 'NO_ROLES',
221
+ });
222
+ }
223
+ for (const method of res.methods) {
224
+ if (method.roles.length === 0 && res.classRoles.length === 0) {
225
+ warnings.push({
226
+ details: `Method '${method.name}' has no @Roles and class has no @Roles`,
227
+ file: res.filePath,
228
+ module: mod.name,
229
+ type: 'UNRESTRICTED_METHOD',
230
+ });
231
+ }
232
+ }
233
+ }
234
+ }
235
+ for (const obj of objects) {
236
+ for (const field of obj.fields) {
237
+ if (field.roles === '*(none)*') {
238
+ warnings.push({
239
+ details: `Field '${field.name}' has no role restriction`,
240
+ file: obj.filePath,
241
+ module: 'objects',
242
+ type: 'UNRESTRICTED_FIELD',
243
+ });
244
+ }
245
+ }
246
+ }
247
+ return warnings;
248
+ }
249
+ function discoverModules(modulesDir) {
250
+ if (!(0, fs_1.existsSync)(modulesDir))
251
+ return [];
252
+ return (0, fs_1.readdirSync)(modulesDir)
253
+ .filter((item) => (0, fs_1.statSync)((0, path_1.join)(modulesDir, item)).isDirectory())
254
+ .sort();
255
+ }
256
+ function findProjectRoot(startPath) {
257
+ let current = startPath || process.cwd();
258
+ for (let i = 0; i < 10; i++) {
259
+ if ((0, fs_1.existsSync)((0, path_1.join)(current, 'src', 'server', 'modules'))) {
260
+ return current;
261
+ }
262
+ const parent = (0, path_1.join)(current, '..');
263
+ if (parent === current)
264
+ break;
265
+ current = parent;
266
+ }
267
+ return null;
268
+ }
269
+ function scanPermissions(projectPath, logger) {
270
+ const log = logger?.log || (() => { });
271
+ const warn = logger?.warn || (() => { });
272
+ log('Scanning permissions...');
273
+ const project = new ts_morph_1.Project({ compilerOptions: { allowJs: true }, skipAddingFilesFromTsConfig: true });
274
+ const modulesDir = (0, path_1.join)(projectPath, 'src', 'server', 'modules');
275
+ const objectsDir = (0, path_1.join)(projectPath, 'src', 'server', 'common', 'objects');
276
+ preloadBaseClasses(project, projectPath);
277
+ const roleEnums = collectRoleEnums(project, projectPath);
278
+ const moduleNames = discoverModules(modulesDir);
279
+ const modules = [];
280
+ for (const name of moduleNames) {
281
+ try {
282
+ modules.push(scanModule(project, modulesDir, name, projectPath));
283
+ }
284
+ catch (error) {
285
+ warn(`Failed to scan module '${name}': ${error}`);
286
+ }
287
+ }
288
+ const objects = scanObjects(project, objectsDir, projectPath);
289
+ const warnings = detectSecurityGaps(modules, objects);
290
+ const stats = calculateStats(modules, objects, warnings);
291
+ log(`Scan complete: ${modules.length} modules, ${objects.length} objects, ${warnings.length} warnings`);
292
+ return {
293
+ generated: new Date().toISOString(),
294
+ modules,
295
+ objects,
296
+ roleEnums,
297
+ stats,
298
+ warnings,
299
+ };
300
+ }
301
+ function scanModule(project, modulesDir, moduleName, projectPath) {
302
+ const moduleDir = (0, path_1.join)(modulesDir, moduleName);
303
+ const result = {
304
+ controllers: [],
305
+ inputs: [],
306
+ models: [],
307
+ name: moduleName,
308
+ outputs: [],
309
+ resolvers: [],
310
+ };
311
+ for (const file of listDir(moduleDir).filter((f) => f.endsWith('.model.ts'))) {
312
+ try {
313
+ const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file));
314
+ const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file)), true);
315
+ if (perms) {
316
+ const classDecl = sf.getClasses()[0];
317
+ if (classDecl) {
318
+ const inheritedFields = resolveInheritedFields(project, classDecl);
319
+ const localNames = new Set(perms.fields.map((f) => f.name));
320
+ for (const iField of inheritedFields) {
321
+ if (!localNames.has(iField.name))
322
+ perms.fields.push(iField);
323
+ }
324
+ }
325
+ result.models.push(perms);
326
+ }
327
+ }
328
+ catch {
329
+ }
330
+ }
331
+ const inputDir = (0, path_1.join)(moduleDir, 'inputs');
332
+ for (const file of listDir(inputDir).filter((f) => f.endsWith('.input.ts'))) {
333
+ try {
334
+ const sf = project.addSourceFileAtPath((0, path_1.join)(inputDir, file));
335
+ const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(inputDir, file)), false);
336
+ if (perms)
337
+ result.inputs.push(perms);
338
+ }
339
+ catch {
340
+ }
341
+ }
342
+ const outputDir = (0, path_1.join)(moduleDir, 'outputs');
343
+ for (const file of listDir(outputDir).filter((f) => f.endsWith('.output.ts'))) {
344
+ try {
345
+ const sf = project.addSourceFileAtPath((0, path_1.join)(outputDir, file));
346
+ const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(outputDir, file)), false);
347
+ if (perms)
348
+ result.outputs.push(perms);
349
+ }
350
+ catch {
351
+ }
352
+ }
353
+ for (const file of listDir(moduleDir).filter((f) => f.endsWith('.controller.ts'))) {
354
+ try {
355
+ const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file));
356
+ const perms = parseEndpointPermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file)));
357
+ if (perms)
358
+ result.controllers.push(perms);
359
+ }
360
+ catch {
361
+ }
362
+ }
363
+ for (const file of listDir(moduleDir).filter((f) => f.endsWith('.resolver.ts'))) {
364
+ try {
365
+ const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file));
366
+ const perms = parseEndpointPermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file)));
367
+ if (perms)
368
+ result.resolvers.push(perms);
369
+ }
370
+ catch {
371
+ }
372
+ }
373
+ return result;
374
+ }
375
+ function scanObjects(project, objectsDir, projectPath) {
376
+ const objects = [];
377
+ if (!(0, fs_1.existsSync)(objectsDir))
378
+ return objects;
379
+ for (const dir of listDir(objectsDir)) {
380
+ const dirPath = (0, path_1.join)(objectsDir, dir);
381
+ try {
382
+ if (!(0, fs_1.statSync)(dirPath).isDirectory())
383
+ continue;
384
+ }
385
+ catch {
386
+ continue;
387
+ }
388
+ for (const file of listDir(dirPath).filter((f) => f.endsWith('.object.ts'))) {
389
+ try {
390
+ const sf = project.addSourceFileAtPath((0, path_1.join)(dirPath, file));
391
+ const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(dirPath, file)), false);
392
+ if (perms)
393
+ objects.push(perms);
394
+ }
395
+ catch {
396
+ }
397
+ }
398
+ }
399
+ return objects;
400
+ }
401
+ function extractDecoratorRoles(decoratorArgs) {
402
+ const roles = [];
403
+ for (const arg of decoratorArgs) {
404
+ const cleaned = arg.trim();
405
+ if (cleaned.startsWith('[')) {
406
+ const inner = cleaned.slice(1, -1);
407
+ for (const item of inner.split(',')) {
408
+ roles.push(formatRole(item.trim()));
409
+ }
410
+ }
411
+ else {
412
+ roles.push(formatRole(cleaned));
413
+ }
414
+ }
415
+ return roles;
416
+ }
417
+ function extractSecurityCheckInfo(classDecl) {
418
+ const method = classDecl.getMethod('securityCheck');
419
+ if (!method)
420
+ return undefined;
421
+ const body = method.getBodyText() || '';
422
+ const fieldsStripped = [];
423
+ const deleteMatches = body.matchAll(/delete\s+\w+\.(\w+)/g);
424
+ for (const m of deleteMatches)
425
+ fieldsStripped.push(m[1]);
426
+ const arrayMatches = body.matchAll(/(?:fieldsToRemove|removeFields|stripFields|fieldsToStrip)\s*=\s*\[([^\]]+)\]/g);
427
+ for (const m of arrayMatches) {
428
+ fieldsStripped.push(...m[1].split(',').map((f) => f.trim().replace(/['"]/g, '')));
429
+ }
430
+ const graphqlFieldMatches = body.matchAll(/(?:removeKeys|filterKeys)\s*\([^,]*,\s*\[([^\]]+)\]/g);
431
+ for (const m of graphqlFieldMatches) {
432
+ fieldsStripped.push(...m[1].split(',').map((f) => f.trim().replace(/['"]/g, '')));
433
+ }
434
+ const returnsUndefined = body.includes('return undefined') || body.includes('return null');
435
+ const summaryParts = ['Present'];
436
+ if (fieldsStripped.length > 0)
437
+ summaryParts.push(`Strips fields: ${fieldsStripped.join(', ')}`);
438
+ if (returnsUndefined)
439
+ summaryParts.push('May return undefined');
440
+ return { fieldsStripped: [...new Set(fieldsStripped)], returnsUndefined, summary: summaryParts.join('. ') };
441
+ }
442
+ function formatRole(role) {
443
+ if (!role)
444
+ return '';
445
+ const dotIndex = role.lastIndexOf('.');
446
+ return dotIndex >= 0 ? role.substring(dotIndex + 1) : role;
447
+ }
448
+ function formatRolesDisplay(roles) {
449
+ if (roles.length === 0)
450
+ return '*(none)*';
451
+ if (roles.length === 1)
452
+ return `\`${roles[0]}\``;
453
+ return roles.map((r) => `\`${r}\``).join(', ');
454
+ }
455
+ function generateMarkdownReport(report, projectPath) {
456
+ const lines = [];
457
+ lines.push('# Permissions Report');
458
+ lines.push('');
459
+ lines.push(`> Generated: ${report.generated}`);
460
+ if (projectPath)
461
+ lines.push(`> Project: ${projectPath}`);
462
+ lines.push(`> Modules: ${report.stats.totalModules} | Models: ${report.stats.totalModels} | Endpoints: ${report.stats.totalEndpoints} | SubObjects: ${report.stats.totalSubObjects}`);
463
+ lines.push(`> Warnings: ${report.stats.totalWarnings} | Endpoint Coverage: ${report.stats.endpointCoverage}% | Security Coverage: ${report.stats.securityCoverage}%`);
464
+ lines.push('');
465
+ lines.push('## Table of Contents');
466
+ lines.push('');
467
+ lines.push('- [Role Index](#role-index)');
468
+ lines.push('- [Summary](#summary)');
469
+ lines.push('- [Warnings](#warnings)');
470
+ for (const mod of report.modules) {
471
+ const anchor = mod.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
472
+ lines.push(`- [Module: ${mod.name}](#module-${anchor})`);
473
+ }
474
+ if (report.objects.length > 0) {
475
+ lines.push('- [SubObjects](#subobjects)');
476
+ }
477
+ lines.push('');
478
+ lines.push('## Role Index');
479
+ lines.push('');
480
+ if (report.roleEnums.length > 0) {
481
+ lines.push('| Enum | Value | Type |');
482
+ lines.push('|------|-------|------|');
483
+ for (const enumInfo of report.roleEnums) {
484
+ for (const v of enumInfo.values) {
485
+ const isSystem = v.key.startsWith('S_');
486
+ lines.push(`| ${enumInfo.name}.${v.key} | ${isSystem ? '*(system)*' : `\`${v.value}\``} | ${isSystem ? 'System' : 'Real'} |`);
487
+ }
488
+ }
489
+ }
490
+ else {
491
+ lines.push('*No role enums found.*');
492
+ }
493
+ lines.push('');
494
+ lines.push('## Summary');
495
+ lines.push('');
496
+ lines.push('| Module | Models | Inputs | Outputs | Controllers | Resolvers | Warnings |');
497
+ lines.push('|--------|--------|--------|---------|-------------|-----------|----------|');
498
+ for (const mod of report.modules) {
499
+ const modWarnings = report.warnings.filter((w) => w.module === mod.name).length;
500
+ lines.push(`| ${mod.name} | ${mod.models.length} | ${mod.inputs.length} | ${mod.outputs.length} | ${mod.controllers.length} | ${mod.resolvers.length} | ${modWarnings} |`);
501
+ }
502
+ lines.push('');
503
+ if (report.stats.totalWarnings > 0) {
504
+ lines.push('### Warnings by Type');
505
+ lines.push('');
506
+ lines.push('| Type | Count |');
507
+ lines.push('|------|-------|');
508
+ for (const [type, count] of Object.entries(report.stats.warningsByType)) {
509
+ if (count > 0)
510
+ lines.push(`| ${type} | ${count} |`);
511
+ }
512
+ lines.push('');
513
+ }
514
+ lines.push('## Warnings');
515
+ lines.push('');
516
+ if (report.warnings.length > 0) {
517
+ lines.push('| # | Module | File | Type | Details |');
518
+ lines.push('|---|--------|------|------|---------|');
519
+ report.warnings.forEach((w, i) => {
520
+ const fileName = w.file.split('/').pop() || w.file;
521
+ lines.push(`| ${i + 1} | ${w.module} | ${fileName} | ${w.type} | ${w.details} |`);
522
+ });
523
+ }
524
+ else {
525
+ lines.push('*No warnings found.*');
526
+ }
527
+ lines.push('');
528
+ for (const mod of report.modules) {
529
+ lines.push('---');
530
+ lines.push('');
531
+ lines.push(`## Module: ${mod.name}`);
532
+ lines.push('');
533
+ for (const model of mod.models) {
534
+ lines.push(`### Model: ${model.className}`);
535
+ lines.push(`- **File:** \`${model.filePath}\``);
536
+ if (model.extendsClass)
537
+ lines.push(`- **Extends:** \`${model.extendsClass}\``);
538
+ lines.push(`- **Class Restriction:** ${model.classRestriction.length > 0 ? model.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
539
+ if (model.securityCheck) {
540
+ lines.push(`- **securityCheck:** ${model.securityCheck.summary}`);
541
+ }
542
+ else {
543
+ lines.push('- **securityCheck:** Not present');
544
+ }
545
+ lines.push('');
546
+ if (model.fields.length > 0) {
547
+ lines.push('| Field | Roles | Source |');
548
+ lines.push('|-------|-------|--------|');
549
+ for (const field of model.fields) {
550
+ const source = field.inherited ? 'inherited' : 'local';
551
+ lines.push(`| ${field.name} | ${field.roles} | ${source} |`);
552
+ }
553
+ }
554
+ lines.push('');
555
+ }
556
+ for (const input of mod.inputs) {
557
+ lines.push(`### Input: ${input.className}`);
558
+ lines.push(`- **File:** \`${input.filePath}\``);
559
+ if (input.extendsClass)
560
+ lines.push(`- **Extends:** \`${input.extendsClass}\``);
561
+ lines.push(`- **Class Restriction:** ${input.classRestriction.length > 0 ? input.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
562
+ lines.push('');
563
+ if (input.fields.length > 0) {
564
+ lines.push('| Field | Roles |');
565
+ lines.push('|-------|-------|');
566
+ for (const field of input.fields) {
567
+ lines.push(`| ${field.name} | ${field.roles} |`);
568
+ }
569
+ }
570
+ lines.push('');
571
+ }
572
+ for (const output of mod.outputs) {
573
+ lines.push(`### Output: ${output.className}`);
574
+ lines.push(`- **File:** \`${output.filePath}\``);
575
+ if (output.extendsClass)
576
+ lines.push(`- **Extends:** \`${output.extendsClass}\``);
577
+ lines.push('');
578
+ if (output.fields.length > 0) {
579
+ lines.push('| Field | Roles |');
580
+ lines.push('|-------|-------|');
581
+ for (const field of output.fields) {
582
+ lines.push(`| ${field.name} | ${field.roles} |`);
583
+ }
584
+ }
585
+ lines.push('');
586
+ }
587
+ for (const ctrl of mod.controllers) {
588
+ lines.push(`### Controller: ${ctrl.className}`);
589
+ lines.push(`- **File:** \`${ctrl.filePath}\``);
590
+ lines.push(`- **Class Roles:** ${ctrl.classRoles.length > 0 ? ctrl.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
591
+ lines.push('');
592
+ if (ctrl.methods.length > 0) {
593
+ lines.push('| Method | HTTP | Route | Roles | Effective |');
594
+ lines.push('|--------|------|-------|-------|-----------|');
595
+ for (const m of ctrl.methods) {
596
+ const effective = m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(ctrl.classRoles)} (class)`;
597
+ lines.push(`| ${m.name} | ${m.httpMethod} | ${m.route || '/'} | ${formatRolesDisplay(m.roles)} | ${effective} |`);
598
+ }
599
+ }
600
+ lines.push('');
601
+ }
602
+ for (const res of mod.resolvers) {
603
+ lines.push(`### Resolver: ${res.className}`);
604
+ lines.push(`- **File:** \`${res.filePath}\``);
605
+ lines.push(`- **Class Roles:** ${res.classRoles.length > 0 ? res.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
606
+ lines.push('');
607
+ if (res.methods.length > 0) {
608
+ lines.push('| Method | Type | Roles | Effective |');
609
+ lines.push('|--------|------|-------|-----------|');
610
+ for (const m of res.methods) {
611
+ const effective = m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(res.classRoles)} (class)`;
612
+ lines.push(`| ${m.name} | ${m.httpMethod} | ${formatRolesDisplay(m.roles)} | ${effective} |`);
613
+ }
614
+ }
615
+ lines.push('');
616
+ }
617
+ const matrix = buildEffectiveMatrix(mod);
618
+ if (matrix.length > 0) {
619
+ lines.push(`### Effective Permissions: ${mod.name}`);
620
+ lines.push('');
621
+ lines.push('| Role | Endpoint Access |');
622
+ lines.push('|------|-----------------|');
623
+ for (const entry of matrix) {
624
+ const endpointList = entry.endpoints.map((e) => `${e.method} ${e.name}`).join(', ');
625
+ lines.push(`| \`${entry.role}\` | ${endpointList || '*(none)*'} |`);
626
+ }
627
+ lines.push('');
628
+ }
629
+ }
630
+ if (report.objects.length > 0) {
631
+ lines.push('---');
632
+ lines.push('');
633
+ lines.push('## SubObjects');
634
+ lines.push('');
635
+ for (const obj of report.objects) {
636
+ lines.push(`### ${obj.className}`);
637
+ lines.push(`- **File:** \`${obj.filePath}\``);
638
+ if (obj.extendsClass)
639
+ lines.push(`- **Extends:** \`${obj.extendsClass}\``);
640
+ lines.push(`- **Class Restriction:** ${obj.classRestriction.length > 0 ? obj.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
641
+ lines.push('');
642
+ if (obj.fields.length > 0) {
643
+ lines.push('| Field | Roles | Source |');
644
+ lines.push('|-------|-------|--------|');
645
+ for (const field of obj.fields) {
646
+ const source = field.inherited ? 'inherited' : 'local';
647
+ lines.push(`| ${field.name} | ${field.roles} | ${source} |`);
648
+ }
649
+ }
650
+ lines.push('');
651
+ }
652
+ }
653
+ return lines.join('\n');
654
+ }
655
+ function parseEndpointPermissions(sourceFile, filePath) {
656
+ const classes = sourceFile.getClasses();
657
+ if (classes.length === 0)
658
+ return undefined;
659
+ const classDecl = classes[0];
660
+ const className = classDecl.getName() || 'Unknown';
661
+ const classRoles = parseRolesDecorator(classDecl);
662
+ let controllerPrefix = '';
663
+ const controllerDeco = classDecl.getDecorator('Controller');
664
+ if (controllerDeco) {
665
+ const args = controllerDeco.getArguments();
666
+ if (args.length > 0)
667
+ controllerPrefix = args[0].getText().replace(/['"]/g, '');
668
+ }
669
+ const methods = [];
670
+ for (const method of classDecl.getMethods()) {
671
+ const methodName = method.getName();
672
+ if (methodName.startsWith('_') || ['onModuleDestroy', 'onModuleInit'].includes(methodName))
673
+ continue;
674
+ const methodRoles = parseRolesDecorator(method);
675
+ for (const httpDeco of ['Delete', 'Get', 'Patch', 'Post', 'Put']) {
676
+ const deco = method.getDecorator(httpDeco);
677
+ if (deco) {
678
+ const args = deco.getArguments();
679
+ const route = args.length > 0 ? args[0].getText().replace(/['"]/g, '') : '/';
680
+ const fullRoute = controllerPrefix
681
+ ? `/${controllerPrefix}/${route}`.replace(/\/+/g, '/')
682
+ : `/${route}`.replace(/\/+/g, '/');
683
+ methods.push({ httpMethod: httpDeco.toUpperCase(), name: methodName, roles: methodRoles, route: fullRoute });
684
+ }
685
+ }
686
+ for (const gqlDeco of ['Mutation', 'Query', 'Subscription']) {
687
+ const deco = method.getDecorator(gqlDeco);
688
+ if (deco) {
689
+ methods.push({ httpMethod: gqlDeco, name: methodName, roles: methodRoles });
690
+ }
691
+ }
692
+ }
693
+ return { className, classRoles, filePath, methods };
694
+ }
695
+ function parseFieldPermission(prop) {
696
+ const fieldName = prop.getName();
697
+ let roles = '*(none)*';
698
+ let description;
699
+ const unifiedField = prop.getDecorator('UnifiedField');
700
+ if (unifiedField) {
701
+ const args = unifiedField.getArguments();
702
+ if (args.length > 0) {
703
+ const optionsArg = args[0];
704
+ if (optionsArg.getKind() === ts_morph_1.SyntaxKind.ObjectLiteralExpression) {
705
+ const objLit = optionsArg.asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
706
+ const rolesProp = objLit?.getProperty('roles');
707
+ if (rolesProp) {
708
+ const init = rolesProp.asKind(ts_morph_1.SyntaxKind.PropertyAssignment)?.getInitializer();
709
+ if (init) {
710
+ const rolesText = init.getText();
711
+ if (rolesText.startsWith('[')) {
712
+ const inner = rolesText.slice(1, -1);
713
+ const roleList = inner.split(',').map((r) => formatRole(r.trim()));
714
+ roles = roleList.map((r) => `\`${r}\``).join(', ');
715
+ }
716
+ else {
717
+ roles = `\`${formatRole(rolesText)}\``;
718
+ }
719
+ }
720
+ }
721
+ const descProp = objLit?.getProperty('description');
722
+ if (descProp) {
723
+ const init = descProp.asKind(ts_morph_1.SyntaxKind.PropertyAssignment)?.getInitializer();
724
+ if (init)
725
+ description = init.getText().replace(/^['"]|['"]$/g, '');
726
+ }
727
+ }
728
+ }
729
+ }
730
+ if (roles === '*(none)*') {
731
+ const restricted = prop.getDecorator('Restricted');
732
+ if (restricted) {
733
+ const args = restricted.getArguments().map((a) => a.getText());
734
+ if (args.length > 0) {
735
+ const roleList = extractDecoratorRoles(args);
736
+ roles = roleList.map((r) => `\`${r}\``).join(', ');
737
+ }
738
+ }
739
+ }
740
+ return { description, name: fieldName, roles };
741
+ }
742
+ function parseFilePermissions(sourceFile, filePath, isModel) {
743
+ const classes = sourceFile.getClasses();
744
+ if (classes.length === 0)
745
+ return undefined;
746
+ const classDecl = classes[0];
747
+ const className = classDecl.getName() || 'Unknown';
748
+ const extendsExpr = classDecl.getExtends();
749
+ const extendsClass = extendsExpr?.getText()?.replace(/<.*>/, '') || undefined;
750
+ const classRestriction = parseRestrictedDecorator(classDecl);
751
+ const securityCheck = isModel ? extractSecurityCheckInfo(classDecl) : undefined;
752
+ const fields = [];
753
+ for (const prop of classDecl.getProperties()) {
754
+ fields.push(parseFieldPermission(prop));
755
+ }
756
+ return { className, classRestriction, extendsClass, fields, filePath, securityCheck };
757
+ }
758
+ function parseRestrictedDecorator(node) {
759
+ const restricted = node.getDecorator('Restricted');
760
+ if (!restricted)
761
+ return [];
762
+ const args = restricted.getArguments().map((a) => a.getText());
763
+ return extractDecoratorRoles(args);
764
+ }
765
+ function parseRolesDecorator(node) {
766
+ const roles = node.getDecorator('Roles');
767
+ if (!roles)
768
+ return [];
769
+ const args = roles.getArguments().map((a) => a.getText());
770
+ return extractDecoratorRoles(args);
771
+ }
772
+ function resolveInheritedFields(project, classDecl) {
773
+ const inherited = [];
774
+ const extendsExpr = classDecl.getExtends();
775
+ if (!extendsExpr)
776
+ return inherited;
777
+ const baseClassName = extendsExpr.getText().replace(/<.*>/, '');
778
+ for (const sf of project.getSourceFiles()) {
779
+ for (const cls of sf.getClasses()) {
780
+ if (cls.getName() === baseClassName) {
781
+ for (const prop of cls.getProperties()) {
782
+ const field = parseFieldPermission(prop);
783
+ field.inherited = true;
784
+ inherited.push(field);
785
+ }
786
+ const parentFields = resolveInheritedFields(project, cls);
787
+ inherited.push(...parentFields);
788
+ return inherited;
789
+ }
790
+ }
791
+ }
792
+ return inherited;
793
+ }
794
+ function preloadBaseClasses(project, projectPath) {
795
+ const nestServerPaths = [
796
+ (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'src'),
797
+ (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist'),
798
+ ];
799
+ for (const basePath of nestServerPaths) {
800
+ try {
801
+ if ((0, fs_1.existsSync)(basePath)) {
802
+ project.addSourceFilesAtPaths((0, path_1.join)(basePath, '**', '*.ts'));
803
+ }
804
+ }
805
+ catch {
806
+ }
807
+ }
808
+ }
809
+ function listDir(dir) {
810
+ try {
811
+ return (0, fs_1.readdirSync)(dir);
812
+ }
813
+ catch {
814
+ return [];
815
+ }
816
+ }
817
+ //# sourceMappingURL=permissions-scanner.js.map