fix: harden tenant auth and quality gates
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { basename, resolve } from 'node:path';
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..');
|
||||
const dist = resolve(root, 'dist');
|
||||
const html = readFileSync(resolve(dist, 'index.html'), 'utf8');
|
||||
const entryMatch = html.match(/<script[^>]+src="([^"]+\.js)"/);
|
||||
if (!entryMatch) throw new Error('Unable to locate the Vite entry script in dist/index.html');
|
||||
|
||||
const assets = resolve(dist, 'assets');
|
||||
const files = readdirSync(assets).filter((file) => file.endsWith('.js'));
|
||||
const sizes = files.map((file) => ({ file, gzip: gzipSync(readFileSync(resolve(assets, file))).length }));
|
||||
const entryName = basename(entryMatch[1]);
|
||||
const entry = sizes.find((item) => item.file === entryName);
|
||||
if (!entry) throw new Error(`Entry asset ${entryName} was not found`);
|
||||
|
||||
const entryBudget = Number(process.env.BUNDLE_ENTRY_GZIP_BUDGET ?? 250 * 1024);
|
||||
// ECharts core + the three chart types used by the platform currently settle at ~182 KiB.
|
||||
// Keep a narrow calibrated ceiling so future chart imports cannot silently restore the full bundle.
|
||||
const chunkBudget = Number(process.env.BUNDLE_CHUNK_GZIP_BUDGET ?? 190 * 1024);
|
||||
const oversized = sizes.filter((item) => item.file !== entryName && item.gzip > chunkBudget);
|
||||
console.log(`entry ${entry.file}: ${(entry.gzip / 1024).toFixed(2)} KiB gzip (budget ${(entryBudget / 1024).toFixed(0)} KiB)`);
|
||||
for (const item of sizes.toSorted((a, b) => b.gzip - a.gzip).slice(0, 10)) {
|
||||
console.log(`${item.file}: ${(item.gzip / 1024).toFixed(2)} KiB gzip`);
|
||||
}
|
||||
if (entry.gzip > entryBudget || oversized.length) {
|
||||
if (oversized.length) console.error(`Oversized async chunks: ${oversized.map((item) => item.file).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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 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 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');
|
||||
}
|
||||
|
||||
if (violations.length) {
|
||||
console.error(violations.map((item) => `ERROR: ${item}`).join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Code quality structural checks passed.');
|
||||
@@ -7,7 +7,8 @@ const rootLock = JSON.parse(readFileSync(join(workspaceRoot, 'package-lock.json'
|
||||
const apiLock = JSON.parse(readFileSync(join(workspaceRoot, 'api', 'package-lock.json'), 'utf8'));
|
||||
|
||||
assertVersionAtLeast(rootLock.packages['node_modules/postcss']?.version, [8, 5, 18], 'postcss');
|
||||
assertEqual(rootLock.packages['node_modules/react-router']?.version, '7.18.1', 'react-router');
|
||||
assertVersionAtLeast(rootLock.packages['node_modules/react-router']?.version, [7, 18, 2], 'react-router');
|
||||
assertVersionAtLeast(rootLock.packages['node_modules/nanoid']?.version, [3, 3, 18], 'nanoid');
|
||||
assertEqual(apiLock.packages['node_modules/brace-expansion-safe']?.version, '5.0.8', 'brace-expansion-safe');
|
||||
assertEqual(
|
||||
apiLock.packages['node_modules/brace-expansion']?.resolved,
|
||||
@@ -50,7 +51,7 @@ for (const relativePath of [
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Dependency mitigations verified: PostCSS patched, React Router RSC unused, brace expansion bounded and compatible.');
|
||||
console.log('Dependency mitigations verified: PostCSS, React Router and NanoID patched; RSC unused; brace expansion bounded and compatible.');
|
||||
|
||||
function sourceFiles(directory) {
|
||||
return readdirSync(directory).flatMap((name) => {
|
||||
|
||||
Reference in New Issue
Block a user