@lenne.tech/cli
Version:
lenne.Tech CLI: lt
852 lines (851 loc) • 35.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildEffectiveMatrix = buildEffectiveMatrix;
exports.calculateStats = calculateStats;
exports.collectRoleEnums = collectRoleEnums;
exports.detectSecurityGaps = detectSecurityGaps;
exports.discoverModules = discoverModules;
exports.extractDecoratorRoles = extractDecoratorRoles;
exports.extractSecurityCheckInfo = extractSecurityCheckInfo;
exports.formatRole = formatRole;
exports.formatRolesDisplay = formatRolesDisplay;
exports.generateMarkdownReport = generateMarkdownReport;
exports.parseEndpointPermissions = parseEndpointPermissions;
exports.parseFieldPermission = parseFieldPermission;
exports.parseFilePermissions = parseFilePermissions;
exports.parseRestrictedDecorator = parseRestrictedDecorator;
exports.parseRolesDecorator = parseRolesDecorator;
exports.resolveInheritedFields = resolveInheritedFields;
exports.scanModule = scanModule;
exports.scanObjects = scanObjects;
exports.scanPermissions = scanPermissions;
/**
* Fallback permissions scanner for CLI usage.
*
* This is a standalone copy of the scanner from @lenne.tech/nest-server
* (src/core/modules/permissions/permissions-scanner.ts).
*
* It is used as a fallback when the project's nest-server does not include
* the permissions scanner (versions < 11.17.0).
*
* Prefer the nest-server scanner when available — it is the single source of truth.
*/
const fs_1 = require("fs");
const path_1 = require("path");
const ts_morph_1 = require("ts-morph");
// ─────────────────────────────────────────────────────────────────────────────
// Exported functions (alphabetically sorted per ESLint perfectionist rules)
// ─────────────────────────────────────────────────────────────────────────────
function buildEffectiveMatrix(mod) {
const allRoles = new Set();
for (const ctrl of mod.controllers) {
for (const r of ctrl.classRoles)
allRoles.add(r);
for (const m of ctrl.methods) {
for (const r of m.roles)
allRoles.add(r);
}
}
for (const res of mod.resolvers) {
for (const r of res.classRoles)
allRoles.add(r);
for (const m of res.methods) {
for (const r of m.roles)
allRoles.add(r);
}
}
const result = [];
for (const role of [...allRoles].sort()) {
const endpoints = [];
for (const ctrl of mod.controllers) {
for (const m of ctrl.methods) {
const effective = m.roles.length > 0 ? m.roles : ctrl.classRoles;
if (effective.includes(role)) {
endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Controller' });
}
}
}
for (const res of mod.resolvers) {
for (const m of res.methods) {
const effective = m.roles.length > 0 ? m.roles : res.classRoles;
if (effective.includes(role)) {
endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Resolver' });
}
}
}
result.push({ endpoints, role });
}
return result;
}
function calculateStats(modules, objects, warnings) {
const warningsByType = {
NO_RESTRICTION: 0,
NO_ROLES: 0,
NO_SECURITY_CHECK: 0,
UNRESTRICTED_FIELD: 0,
UNRESTRICTED_METHOD: 0,
};
for (const w of warnings) {
if (w.type in warningsByType) {
warningsByType[w.type]++;
}
}
const totalModels = modules.reduce((s, m) => s + m.models.length, 0);
let totalMethods = 0;
let methodsWithRoles = 0;
for (const mod of modules) {
for (const ctrl of mod.controllers) {
for (const m of ctrl.methods) {
totalMethods++;
if (m.roles.length > 0 || ctrl.classRoles.length > 0)
methodsWithRoles++;
}
}
for (const res of mod.resolvers) {
for (const m of res.methods) {
totalMethods++;
if (m.roles.length > 0 || res.classRoles.length > 0)
methodsWithRoles++;
}
}
}
let modelsWithBothChecks = 0;
for (const mod of modules) {
for (const model of mod.models) {
if (model.classRestriction.length > 0 && model.securityCheck) {
modelsWithBothChecks++;
}
}
}
const endpointCoverage = totalMethods > 0 ? Math.round((methodsWithRoles / totalMethods) * 100) : 100;
const securityCoverage = totalModels > 0 ? Math.round((modelsWithBothChecks / totalModels) * 100) : 100;
return {
endpointCoverage,
securityCoverage,
totalEndpoints: totalMethods,
totalModels,
totalModules: modules.length,
totalSubObjects: objects.length,
totalWarnings: warnings.length,
warningsByType,
};
}
function collectRoleEnums(project, projectPath) {
const enums = [];
const enumPatterns = [
(0, path_1.join)(projectPath, 'src', 'server', 'common', 'enums'),
(0, path_1.join)(projectPath, 'src', 'server', 'modules'),
];
for (const dir of enumPatterns) {
try {
const enumFiles = project.addSourceFilesAtPaths((0, path_1.join)(dir, '**', '*.enum.ts'));
for (const sf of enumFiles) {
for (const enumDecl of sf.getEnums()) {
const enumName = enumDecl.getName();
if (enumName.toLowerCase().includes('role')) {
const values = enumDecl.getMembers().map((m) => {
var _a;
return ({
key: m.getName(),
value: ((_a = m.getValue()) === null || _a === void 0 ? void 0 : _a.toString()) || m.getName(),
});
});
enums.push({
file: (0, path_1.relative)(projectPath, sf.getFilePath()),
name: enumName,
values,
});
}
}
}
}
catch (_a) {
// Directory may not exist
}
}
return enums;
}
function detectSecurityGaps(modules, objects) {
const warnings = [];
for (const mod of modules) {
for (const model of mod.models) {
if (model.classRestriction.length === 0) {
warnings.push({
details: `Model ${model.className} has no @Restricted class-level restriction`,
file: model.filePath,
module: mod.name,
type: 'NO_RESTRICTION',
});
}
if (!model.securityCheck) {
warnings.push({
details: `Model ${model.className} has no securityCheck override`,
file: model.filePath,
module: mod.name,
type: 'NO_SECURITY_CHECK',
});
}
for (const field of model.fields) {
if (field.roles === '*(none)*') {
warnings.push({
details: `Field '${field.name}' has no role restriction`,
file: model.filePath,
module: mod.name,
type: 'UNRESTRICTED_FIELD',
});
}
}
}
for (const input of mod.inputs) {
for (const field of input.fields) {
if (field.roles === '*(none)*') {
warnings.push({
details: `Field '${field.name}' has no role restriction`,
file: input.filePath,
module: mod.name,
type: 'UNRESTRICTED_FIELD',
});
}
}
}
for (const ctrl of mod.controllers) {
if (ctrl.classRoles.length === 0) {
warnings.push({
details: `Controller ${ctrl.className} has no @Roles class-level restriction`,
file: ctrl.filePath,
module: mod.name,
type: 'NO_ROLES',
});
}
for (const method of ctrl.methods) {
if (method.roles.length === 0 && ctrl.classRoles.length === 0) {
warnings.push({
details: `Method '${method.name}' has no @Roles and class has no @Roles`,
file: ctrl.filePath,
module: mod.name,
type: 'UNRESTRICTED_METHOD',
});
}
}
}
for (const res of mod.resolvers) {
if (res.classRoles.length === 0) {
warnings.push({
details: `Resolver ${res.className} has no @Roles class-level restriction`,
file: res.filePath,
module: mod.name,
type: 'NO_ROLES',
});
}
for (const method of res.methods) {
if (method.roles.length === 0 && res.classRoles.length === 0) {
warnings.push({
details: `Method '${method.name}' has no @Roles and class has no @Roles`,
file: res.filePath,
module: mod.name,
type: 'UNRESTRICTED_METHOD',
});
}
}
}
}
for (const obj of objects) {
for (const field of obj.fields) {
if (field.roles === '*(none)*') {
warnings.push({
details: `Field '${field.name}' has no role restriction`,
file: obj.filePath,
module: 'objects',
type: 'UNRESTRICTED_FIELD',
});
}
}
}
return warnings;
}
function discoverModules(modulesDir) {
if (!(0, fs_1.existsSync)(modulesDir))
return [];
return (0, fs_1.readdirSync)(modulesDir)
.filter((item) => (0, fs_1.statSync)((0, path_1.join)(modulesDir, item)).isDirectory())
.sort();
}
function extractDecoratorRoles(decoratorArgs) {
const roles = [];
for (const arg of decoratorArgs) {
const cleaned = arg.trim();
if (cleaned.startsWith('[')) {
const inner = cleaned.slice(1, -1);
for (const item of inner.split(',')) {
roles.push(formatRole(item.trim()));
}
}
else {
roles.push(formatRole(cleaned));
}
}
return roles;
}
function extractSecurityCheckInfo(classDecl) {
const method = classDecl.getMethod('securityCheck');
if (!method)
return undefined;
const body = method.getBodyText() || '';
const fieldsStripped = [];
const deleteMatches = body.matchAll(/delete\s+\w+\.(\w+)/g);
for (const m of deleteMatches)
fieldsStripped.push(m[1]);
const arrayMatches = body.matchAll(/(?:fieldsToRemove|removeFields|stripFields|fieldsToStrip)\s*=\s*\[([^\]]+)\]/g);
for (const m of arrayMatches) {
fieldsStripped.push(...m[1].split(',').map((f) => f.trim().replace(/['"]/g, '')));
}
const graphqlFieldMatches = body.matchAll(/(?:removeKeys|filterKeys)\s*\([^,]*,\s*\[([^\]]+)\]/g);
for (const m of graphqlFieldMatches) {
fieldsStripped.push(...m[1].split(',').map((f) => f.trim().replace(/['"]/g, '')));
}
const returnsUndefined = body.includes('return undefined') || body.includes('return null');
const summaryParts = ['Present'];
if (fieldsStripped.length > 0)
summaryParts.push(`Strips fields: ${fieldsStripped.join(', ')}`);
if (returnsUndefined)
summaryParts.push('May return undefined');
return { fieldsStripped: [...new Set(fieldsStripped)], returnsUndefined, summary: summaryParts.join('. ') };
}
function formatRole(role) {
if (!role)
return '';
const dotIndex = role.lastIndexOf('.');
return dotIndex >= 0 ? role.substring(dotIndex + 1) : role;
}
function formatRolesDisplay(roles) {
if (roles.length === 0)
return '*(none)*';
if (roles.length === 1)
return `\`${roles[0]}\``;
return roles.map((r) => `\`${r}\``).join(', ');
}
function generateMarkdownReport(report, projectPath) {
const lines = [];
// Header
lines.push('# Permissions Report');
lines.push('');
lines.push(`> Generated: ${report.generated}`);
if (projectPath)
lines.push(`> Project: ${projectPath}`);
lines.push(`> Modules: ${report.stats.totalModules} | Models: ${report.stats.totalModels} | Endpoints: ${report.stats.totalEndpoints} | SubObjects: ${report.stats.totalSubObjects}`);
lines.push(`> Warnings: ${report.stats.totalWarnings} | Endpoint Coverage: ${report.stats.endpointCoverage}% | Security Coverage: ${report.stats.securityCoverage}%`);
lines.push('');
// Table of contents
lines.push('## Table of Contents');
lines.push('');
lines.push('- [Role Index](#role-index)');
lines.push('- [Summary](#summary)');
lines.push('- [Warnings](#warnings)');
for (const mod of report.modules) {
const anchor = mod.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
lines.push(`- [Module: ${mod.name}](#module-${anchor})`);
}
if (report.objects.length > 0) {
lines.push('- [SubObjects](#subobjects)');
}
lines.push('');
// Role index
lines.push('## Role Index');
lines.push('');
if (report.roleEnums.length > 0) {
lines.push('| Enum | Value | Type |');
lines.push('|------|-------|------|');
for (const enumInfo of report.roleEnums) {
for (const v of enumInfo.values) {
const isSystem = v.key.startsWith('S_');
lines.push(`| ${enumInfo.name}.${v.key} | ${isSystem ? '*(system)*' : `\`${v.value}\``} | ${isSystem ? 'System' : 'Real'} |`);
}
}
}
else {
lines.push('*No role enums found.*');
}
lines.push('');
// Summary table
lines.push('## Summary');
lines.push('');
lines.push('| Module | Models | Inputs | Outputs | Controllers | Resolvers | Warnings |');
lines.push('|--------|--------|--------|---------|-------------|-----------|----------|');
for (const mod of report.modules) {
const modWarnings = report.warnings.filter((w) => w.module === mod.name).length;
lines.push(`| ${mod.name} | ${mod.models.length} | ${mod.inputs.length} | ${mod.outputs.length} | ${mod.controllers.length} | ${mod.resolvers.length} | ${modWarnings} |`);
}
lines.push('');
if (report.stats.totalWarnings > 0) {
lines.push('### Warnings by Type');
lines.push('');
lines.push('| Type | Count |');
lines.push('|------|-------|');
for (const [type, count] of Object.entries(report.stats.warningsByType)) {
if (count > 0)
lines.push(`| ${type} | ${count} |`);
}
lines.push('');
}
// Warnings
lines.push('## Warnings');
lines.push('');
if (report.warnings.length > 0) {
lines.push('| # | Module | File | Type | Details |');
lines.push('|---|--------|------|------|---------|');
report.warnings.forEach((w, i) => {
const fileName = w.file.split('/').pop() || w.file;
lines.push(`| ${i + 1} | ${w.module} | ${fileName} | ${w.type} | ${w.details} |`);
});
}
else {
lines.push('*No warnings found.*');
}
lines.push('');
// Module details
for (const mod of report.modules) {
lines.push('---');
lines.push('');
lines.push(`## Module: ${mod.name}`);
lines.push('');
// Models
for (const model of mod.models) {
lines.push(`### Model: ${model.className}`);
lines.push(`- **File:** \`${model.filePath}\``);
if (model.extendsClass)
lines.push(`- **Extends:** \`${model.extendsClass}\``);
lines.push(`- **Class Restriction:** ${model.classRestriction.length > 0 ? model.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
if (model.securityCheck) {
lines.push(`- **securityCheck:** ${model.securityCheck.summary}`);
}
else {
lines.push('- **securityCheck:** Not present');
}
lines.push('');
if (model.fields.length > 0) {
lines.push('| Field | Roles | Source |');
lines.push('|-------|-------|--------|');
for (const field of model.fields) {
const source = field.inherited ? 'inherited' : 'local';
lines.push(`| ${field.name} | ${field.roles} | ${source} |`);
}
}
lines.push('');
}
// Inputs
for (const input of mod.inputs) {
lines.push(`### Input: ${input.className}`);
lines.push(`- **File:** \`${input.filePath}\``);
if (input.extendsClass)
lines.push(`- **Extends:** \`${input.extendsClass}\``);
lines.push(`- **Class Restriction:** ${input.classRestriction.length > 0 ? input.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
lines.push('');
if (input.fields.length > 0) {
lines.push('| Field | Roles |');
lines.push('|-------|-------|');
for (const field of input.fields) {
lines.push(`| ${field.name} | ${field.roles} |`);
}
}
lines.push('');
}
// Outputs
for (const output of mod.outputs) {
lines.push(`### Output: ${output.className}`);
lines.push(`- **File:** \`${output.filePath}\``);
if (output.extendsClass)
lines.push(`- **Extends:** \`${output.extendsClass}\``);
lines.push('');
if (output.fields.length > 0) {
lines.push('| Field | Roles |');
lines.push('|-------|-------|');
for (const field of output.fields) {
lines.push(`| ${field.name} | ${field.roles} |`);
}
}
lines.push('');
}
// Controllers
for (const ctrl of mod.controllers) {
lines.push(`### Controller: ${ctrl.className}`);
lines.push(`- **File:** \`${ctrl.filePath}\``);
lines.push(`- **Class Roles:** ${ctrl.classRoles.length > 0 ? ctrl.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
lines.push('');
if (ctrl.methods.length > 0) {
lines.push('| Method | HTTP | Route | Roles | Effective |');
lines.push('|--------|------|-------|-------|-----------|');
for (const m of ctrl.methods) {
const effective = m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(ctrl.classRoles)} (class)`;
lines.push(`| ${m.name} | ${m.httpMethod} | ${m.route || '/'} | ${formatRolesDisplay(m.roles)} | ${effective} |`);
}
}
lines.push('');
}
// Resolvers
for (const res of mod.resolvers) {
lines.push(`### Resolver: ${res.className}`);
lines.push(`- **File:** \`${res.filePath}\``);
lines.push(`- **Class Roles:** ${res.classRoles.length > 0 ? res.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
lines.push('');
if (res.methods.length > 0) {
lines.push('| Method | Type | Roles | Effective |');
lines.push('|--------|------|-------|-----------|');
for (const m of res.methods) {
const effective = m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(res.classRoles)} (class)`;
lines.push(`| ${m.name} | ${m.httpMethod} | ${formatRolesDisplay(m.roles)} | ${effective} |`);
}
}
lines.push('');
}
// Effective matrix
const matrix = buildEffectiveMatrix(mod);
if (matrix.length > 0) {
lines.push(`### Effective Permissions: ${mod.name}`);
lines.push('');
lines.push('| Role | Endpoint Access |');
lines.push('|------|-----------------|');
for (const entry of matrix) {
const endpointList = entry.endpoints.map((e) => `${e.method} ${e.name}`).join(', ');
lines.push(`| \`${entry.role}\` | ${endpointList || '*(none)*'} |`);
}
lines.push('');
}
}
// SubObjects
if (report.objects.length > 0) {
lines.push('---');
lines.push('');
lines.push('## SubObjects');
lines.push('');
for (const obj of report.objects) {
lines.push(`### ${obj.className}`);
lines.push(`- **File:** \`${obj.filePath}\``);
if (obj.extendsClass)
lines.push(`- **Extends:** \`${obj.extendsClass}\``);
lines.push(`- **Class Restriction:** ${obj.classRestriction.length > 0 ? obj.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`);
lines.push('');
if (obj.fields.length > 0) {
lines.push('| Field | Roles | Source |');
lines.push('|-------|-------|--------|');
for (const field of obj.fields) {
const source = field.inherited ? 'inherited' : 'local';
lines.push(`| ${field.name} | ${field.roles} | ${source} |`);
}
}
lines.push('');
}
}
return lines.join('\n');
}
function parseEndpointPermissions(sourceFile, filePath) {
const classes = sourceFile.getClasses();
if (classes.length === 0)
return undefined;
const classDecl = classes[0];
const className = classDecl.getName() || 'Unknown';
const classRoles = parseRolesDecorator(classDecl);
let controllerPrefix = '';
const controllerDeco = classDecl.getDecorator('Controller');
if (controllerDeco) {
const args = controllerDeco.getArguments();
if (args.length > 0)
controllerPrefix = args[0].getText().replace(/['"]/g, '');
}
const methods = [];
for (const method of classDecl.getMethods()) {
const methodName = method.getName();
if (methodName.startsWith('_') || ['onModuleDestroy', 'onModuleInit'].includes(methodName))
continue;
const methodRoles = parseRolesDecorator(method);
for (const httpDeco of ['Delete', 'Get', 'Patch', 'Post', 'Put']) {
const deco = method.getDecorator(httpDeco);
if (deco) {
const args = deco.getArguments();
const route = args.length > 0 ? args[0].getText().replace(/['"]/g, '') : '/';
const fullRoute = controllerPrefix
? `/${controllerPrefix}/${route}`.replace(/\/+/g, '/')
: `/${route}`.replace(/\/+/g, '/');
methods.push({ httpMethod: httpDeco.toUpperCase(), name: methodName, roles: methodRoles, route: fullRoute });
}
}
for (const gqlDeco of ['Mutation', 'Query', 'Subscription']) {
const deco = method.getDecorator(gqlDeco);
if (deco) {
methods.push({ httpMethod: gqlDeco, name: methodName, roles: methodRoles });
}
}
}
return { className, classRoles, filePath, methods };
}
function parseFieldPermission(prop) {
var _a, _b;
const fieldName = prop.getName();
let roles = '*(none)*';
let description;
const unifiedField = prop.getDecorator('UnifiedField');
if (unifiedField) {
const args = unifiedField.getArguments();
if (args.length > 0) {
const optionsArg = args[0];
if (optionsArg.getKind() === ts_morph_1.SyntaxKind.ObjectLiteralExpression) {
const objLit = optionsArg.asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
const rolesProp = objLit === null || objLit === void 0 ? void 0 : objLit.getProperty('roles');
if (rolesProp) {
const init = (_a = rolesProp.asKind(ts_morph_1.SyntaxKind.PropertyAssignment)) === null || _a === void 0 ? void 0 : _a.getInitializer();
if (init) {
const rolesText = init.getText();
if (rolesText.startsWith('[')) {
const inner = rolesText.slice(1, -1);
const roleList = inner.split(',').map((r) => formatRole(r.trim()));
roles = roleList.map((r) => `\`${r}\``).join(', ');
}
else {
roles = `\`${formatRole(rolesText)}\``;
}
}
}
const descProp = objLit === null || objLit === void 0 ? void 0 : objLit.getProperty('description');
if (descProp) {
const init = (_b = descProp.asKind(ts_morph_1.SyntaxKind.PropertyAssignment)) === null || _b === void 0 ? void 0 : _b.getInitializer();
if (init)
description = init.getText().replace(/^['"]|['"]$/g, '');
}
}
}
}
if (roles === '*(none)*') {
const restricted = prop.getDecorator('Restricted');
if (restricted) {
const args = restricted.getArguments().map((a) => a.getText());
if (args.length > 0) {
const roleList = extractDecoratorRoles(args);
roles = roleList.map((r) => `\`${r}\``).join(', ');
}
}
}
return { description, name: fieldName, roles };
}
function parseFilePermissions(sourceFile, filePath, isModel) {
var _a;
const classes = sourceFile.getClasses();
if (classes.length === 0)
return undefined;
const classDecl = classes[0];
const className = classDecl.getName() || 'Unknown';
const extendsExpr = classDecl.getExtends();
const extendsClass = ((_a = extendsExpr === null || extendsExpr === void 0 ? void 0 : extendsExpr.getText()) === null || _a === void 0 ? void 0 : _a.replace(/<.*>/, '')) || undefined;
const classRestriction = parseRestrictedDecorator(classDecl);
const securityCheck = isModel ? extractSecurityCheckInfo(classDecl) : undefined;
const fields = [];
for (const prop of classDecl.getProperties()) {
fields.push(parseFieldPermission(prop));
}
return { className, classRestriction, extendsClass, fields, filePath, securityCheck };
}
function parseRestrictedDecorator(node) {
const restricted = node.getDecorator('Restricted');
if (!restricted)
return [];
const args = restricted.getArguments().map((a) => a.getText());
return extractDecoratorRoles(args);
}
function parseRolesDecorator(node) {
const roles = node.getDecorator('Roles');
if (!roles)
return [];
const args = roles.getArguments().map((a) => a.getText());
return extractDecoratorRoles(args);
}
function resolveInheritedFields(project, classDecl) {
const inherited = [];
const extendsExpr = classDecl.getExtends();
if (!extendsExpr)
return inherited;
const baseClassName = extendsExpr.getText().replace(/<.*>/, '');
for (const sf of project.getSourceFiles()) {
for (const cls of sf.getClasses()) {
if (cls.getName() === baseClassName) {
for (const prop of cls.getProperties()) {
const field = parseFieldPermission(prop);
field.inherited = true;
inherited.push(field);
}
const parentFields = resolveInheritedFields(project, cls);
inherited.push(...parentFields);
return inherited;
}
}
}
return inherited;
}
function scanModule(project, modulesDir, moduleName, projectPath) {
const moduleDir = (0, path_1.join)(modulesDir, moduleName);
const result = {
controllers: [],
inputs: [],
models: [],
name: moduleName,
outputs: [],
resolvers: [],
};
// Models
for (const file of listDir(moduleDir).filter((f) => f.endsWith('.model.ts'))) {
try {
const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file));
const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file)), true);
if (perms) {
const classDecl = sf.getClasses()[0];
if (classDecl) {
const inheritedFields = resolveInheritedFields(project, classDecl);
const localNames = new Set(perms.fields.map((f) => f.name));
for (const iField of inheritedFields) {
if (!localNames.has(iField.name))
perms.fields.push(iField);
}
}
result.models.push(perms);
}
}
catch (_a) {
/* skip */
}
}
// Inputs
const inputDir = (0, path_1.join)(moduleDir, 'inputs');
for (const file of listDir(inputDir).filter((f) => f.endsWith('.input.ts'))) {
try {
const sf = project.addSourceFileAtPath((0, path_1.join)(inputDir, file));
const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(inputDir, file)), false);
if (perms)
result.inputs.push(perms);
}
catch (_b) {
/* skip */
}
}
// Outputs
const outputDir = (0, path_1.join)(moduleDir, 'outputs');
for (const file of listDir(outputDir).filter((f) => f.endsWith('.output.ts'))) {
try {
const sf = project.addSourceFileAtPath((0, path_1.join)(outputDir, file));
const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(outputDir, file)), false);
if (perms)
result.outputs.push(perms);
}
catch (_c) {
/* skip */
}
}
// Controllers
for (const file of listDir(moduleDir).filter((f) => f.endsWith('.controller.ts'))) {
try {
const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file));
const perms = parseEndpointPermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file)));
if (perms)
result.controllers.push(perms);
}
catch (_d) {
/* skip */
}
}
// Resolvers
for (const file of listDir(moduleDir).filter((f) => f.endsWith('.resolver.ts'))) {
try {
const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file));
const perms = parseEndpointPermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file)));
if (perms)
result.resolvers.push(perms);
}
catch (_e) {
/* skip */
}
}
return result;
}
function scanObjects(project, objectsDir, projectPath) {
const objects = [];
if (!(0, fs_1.existsSync)(objectsDir))
return objects;
for (const dir of listDir(objectsDir)) {
const dirPath = (0, path_1.join)(objectsDir, dir);
try {
if (!(0, fs_1.statSync)(dirPath).isDirectory())
continue;
}
catch (_a) {
continue;
}
for (const file of listDir(dirPath).filter((f) => f.endsWith('.object.ts'))) {
try {
const sf = project.addSourceFileAtPath((0, path_1.join)(dirPath, file));
const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(dirPath, file)), false);
if (perms)
objects.push(perms);
}
catch (_b) {
/* skip */
}
}
}
return objects;
}
function scanPermissions(projectPath, logger) {
const log = (logger === null || logger === void 0 ? void 0 : logger.log) || (() => { });
const warn = (logger === null || logger === void 0 ? void 0 : logger.warn) || (() => { });
log('Scanning permissions...');
const project = new ts_morph_1.Project({ compilerOptions: { allowJs: true }, skipAddingFilesFromTsConfig: true });
const modulesDir = (0, path_1.join)(projectPath, 'src', 'server', 'modules');
const objectsDir = (0, path_1.join)(projectPath, 'src', 'server', 'common', 'objects');
// Preload nest-server base classes once (used by resolveInheritedFields for all models)
preloadBaseClasses(project, projectPath);
const roleEnums = collectRoleEnums(project, projectPath);
const moduleNames = discoverModules(modulesDir);
const modules = [];
for (const name of moduleNames) {
try {
modules.push(scanModule(project, modulesDir, name, projectPath));
}
catch (error) {
warn(`Failed to scan module '${name}': ${error}`);
}
}
const objects = scanObjects(project, objectsDir, projectPath);
const warnings = detectSecurityGaps(modules, objects);
const stats = calculateStats(modules, objects, warnings);
log(`Scan complete: ${modules.length} modules, ${objects.length} objects, ${warnings.length} warnings`);
return {
generated: new Date().toISOString(),
modules,
objects,
roleEnums,
stats,
warnings,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Internal helpers
// ─────────────────────────────────────────────────────────────────────────────
function listDir(dir) {
try {
return (0, fs_1.readdirSync)(dir);
}
catch (_a) {
return [];
}
}
function preloadBaseClasses(project, projectPath) {
const nestServerPaths = [
(0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'src'),
(0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist'),
];
for (const basePath of nestServerPaths) {
try {
if ((0, fs_1.existsSync)(basePath)) {
project.addSourceFilesAtPaths((0, path_1.join)(basePath, '**', '*.ts'));
}
}
catch (_a) {
// Base path not available
}
}
}