108 lines
4.5 KiB
JavaScript
108 lines
4.5 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
|
|
const root = resolve(import.meta.dirname, '../..');
|
|
const violations = [];
|
|
|
|
const productionFiles = execFileSync(
|
|
'git',
|
|
['ls-files', 'src/apps/**/*.ts', 'src/apps/**/*.tsx', 'src/components/**/*.ts', 'src/components/**/*.tsx'],
|
|
{ cwd: root, encoding: 'utf8' },
|
|
)
|
|
.split(/\r?\n/)
|
|
.filter(Boolean);
|
|
for (const file of productionFiles) {
|
|
const content = readFileSync(resolve(root, file), 'utf8');
|
|
if (/from\s+['"]@\/mock(?:\/|['"])/.test(content)) violations.push(`${file}: production code imports @/mock`);
|
|
}
|
|
|
|
const clientControllers = execFileSync('git', ['ls-files', 'api/src/**/*.controller.ts'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
})
|
|
.split(/\r?\n/)
|
|
.filter(Boolean);
|
|
for (const file of clientControllers) {
|
|
const content = readFileSync(resolve(root, file), 'utf8');
|
|
const clientClassOffset = content.indexOf('export class Client');
|
|
const clientSection = clientClassOffset >= 0 ? content.slice(clientClassOffset) : content;
|
|
if (/['"]client(?:\/|['"])/.test(clientSection) && /@TenantId\(\)/.test(clientSection)) {
|
|
violations.push(`${file}: client route still reads request-controlled @TenantId()`);
|
|
}
|
|
}
|
|
|
|
const clientApi = readFileSync(resolve(root, 'src/api/client/client.api.ts'), 'utf8');
|
|
if (/DEFAULT_CLIENT_TENANT_ID|getSessionTenantId|x-tenant-id/.test(clientApi)) {
|
|
violations.push(
|
|
'src/api/client/client.api.ts: client requests must derive tenant scope from the authenticated server session',
|
|
);
|
|
}
|
|
const httpClient = readFileSync(resolve(root, 'src/api/core/httpClient.ts'), 'utf8');
|
|
if (
|
|
/DEFAULT_CLIENT_TENANT_ID|getSessionTenantId/.test(httpClient) ||
|
|
!httpClient.includes("options.tenantId && !path.startsWith('/client')")
|
|
) {
|
|
violations.push('src/api/core/httpClient.ts: tenant headers must be reserved for explicit non-client operations');
|
|
}
|
|
|
|
const trackedBuildCaches = execFileSync('git', ['ls-files', '*.tsbuildinfo', '**/*.tsbuildinfo'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
}).trim();
|
|
if (trackedBuildCaches)
|
|
violations.push(`tracked TypeScript build caches: ${trackedBuildCaches.replace(/\r?\n/g, ', ')}`);
|
|
|
|
const usersService = readFileSync(resolve(root, 'api/src/users/users.service.ts'), 'utf8');
|
|
if (/createHash\(['"]sha256['"]\)/.test(usersService)) {
|
|
violations.push('api/src/users/users.service.ts: password writes must use the versioned password hasher');
|
|
}
|
|
const authService = readFileSync(resolve(root, 'api/src/auth/auth.service.ts'), 'utf8');
|
|
if (/passwordHash\s*===|===\s*[^\n;]*passwordHash/.test(authService)) {
|
|
violations.push('api/src/auth/auth.service.ts: password hashes must not be compared directly');
|
|
}
|
|
const ensureAdmin = readFileSync(resolve(root, 'tools/deploy/ensure-production-admin.mjs'), 'utf8');
|
|
if (
|
|
/createHash\(['"]sha256['"]\)|function\s+hashPassword\s*\(/.test(ensureAdmin) ||
|
|
!ensureAdmin.includes('api/dist/auth/password-hasher.js')
|
|
) {
|
|
violations.push(
|
|
'tools/deploy/ensure-production-admin.mjs: administrative password writes must use the API password hasher',
|
|
);
|
|
}
|
|
for (const relativePath of ['tools/deploy/ensure-production-admin.mjs', 'tools/smoke/real-env-smoke.mjs']) {
|
|
const content = readFileSync(resolve(root, relativePath), 'utf8');
|
|
if (
|
|
/function\s+hashPassword\s*\(|passwordHash:\s*(?:createHash|legacyHashPassword)/.test(content) ||
|
|
!content.includes('api/dist/auth/password-hasher.js')
|
|
) {
|
|
violations.push(`${relativePath}: user password writes must use the compiled API password hasher`);
|
|
}
|
|
}
|
|
|
|
const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
|
if (packageJson.dependencies?.['react-router-dom'] !== '7.18.2') {
|
|
violations.push('package.json: react-router-dom must remain on the remediated 7.18.2 baseline');
|
|
}
|
|
if (packageJson.overrides?.nanoid !== '3.3.18') {
|
|
violations.push('package.json: nanoid override must remain on the remediated 3.3.18 baseline');
|
|
}
|
|
const trackedAlternativeLocks = execFileSync('git', ['ls-files', 'pnpm-lock.yaml', 'yarn.lock'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
}).trim();
|
|
if (trackedAlternativeLocks) {
|
|
violations.push(
|
|
`${trackedAlternativeLocks.replace(/\r?\n/g, ', ')}: npm/package-lock.json is the only supported committed dependency lock`,
|
|
);
|
|
}
|
|
if (packageJson.packageManager !== 'npm@11.6.2') {
|
|
violations.push('package.json: packageManager must pin the supported npm baseline');
|
|
}
|
|
|
|
if (violations.length) {
|
|
console.error(violations.map((item) => `ERROR: ${item}`).join('\n'));
|
|
process.exit(1);
|
|
}
|
|
console.log('Code quality structural checks passed.');
|