fix: harden tenant auth and quality gates

This commit is contained in:
hectorzhao
2026-08-28 11:44:16 +08:00
parent c3bf8af3e6
commit 2744690f9f
51 changed files with 1750 additions and 466 deletions
+30
View File
@@ -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);
}
+50
View File
@@ -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) => {