Files
Multi-Hoster-Upload/scripts/verify-public-release.mjs
T

254 lines
8.7 KiB
JavaScript

import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const args = process.argv.slice(2);
const sourceOnly = args.includes('--source-only');
const failures = new Map();
const requiredFiles = [
'.gitignore',
'README.md',
'SECURITY.md',
'package.json',
'package-lock.json',
'eslint.config.mjs',
'main.js',
'preload.js',
'preload-drop-target.js',
'assets/app_icon.ico',
'assets/app_icon.png',
'scripts/afterPack.cjs',
'scripts/verify-public-release.mjs'
];
const allowedFiles = new Set([...requiredFiles, 'assets/product-overview.png']);
const allowedPrefixes = ['lib/', 'renderer/', 'tests/'];
const ignoredDirectories = new Set(['.git', 'node_modules', 'release']);
const deniedDirectories = new Set([
`.${['clau', 'de'].join('')}`,
`.${['co', 'dex'].join('')}`,
'.playwright-mcp',
'.superpowers',
'__pycache__',
'backups',
'docs',
'gateway',
'logs',
'memories',
'prompts',
'tasks'
]);
const deniedBasenames = new Set([
'agents.md',
'app.py',
`${['clau', 'de'].join('')}.md`,
'credentials.json',
'gemini.md',
'hosters.py',
'memory.md',
'memory_summary.md',
'raw_memories.md',
'requirements.txt'
]);
const textExtensions = new Set(['.cjs', '.css', '.html', '.js', '.json', '.md', '.mjs', '.txt', '.yaml', '.yml']);
const binaryExtensions = new Set(['.ico', '.png']);
const expectedScripts = {
start: 'electron .',
test: 'node --test tests/*.test.js tests/ui-smoke.js',
lint: 'eslint .',
dist: 'electron-builder --win',
'release:win': 'electron-builder --publish never --win nsis portable'
};
const expectedBuildFiles = [
'main.js',
'preload.js',
'preload-drop-target.js',
'lib/**/*',
'renderer/**/*',
'assets/app_icon.ico',
'assets/app_icon.png'
];
const aiTerms = [
['clau', 'de'].join(''),
['co', 'dex'].join(''),
['chat', 'gpt'].join('')
].join('|');
const personalTerms = [
['pl', 'oet'].join(''),
['baker', 'edwin318'].join('')
].join('|');
const forbiddenAiPattern = new RegExp(`\\b(?:${aiTerms}|multi[\\s-]+agents?)\\b`, 'i');
const forbiddenPersonalPattern = new RegExp(`(?:[a-z]:[\\\\/]+users[\\\\/]+|\\b(?:${personalTerms})\\b|\\bdesktop-[a-z0-9-]+\\b)`, 'i');
const forbiddenInvestigationPattern = new RegExp(`\\b(?:${['internal', 'investigation'].join(' ')}|${['interne', 'untersuchung'].join(' ')}|${['audit', 'method'].join(' ')}|${['test', 'chronicle'].join(' ')})\\b`, 'i');
const updaterOnlyPattern = new RegExp([
['gi', 'tea'].join(''),
['git', '24-music', 'de'].join('\\.'),
[['Admin', 'istrator'].join(''), 'Multi-Hoster-Upload'].join('\\/')
].join('|'), 'i');
function addFailure(file, rule) {
if (!failures.has(file)) failures.set(file, new Set());
failures.get(file).add(rule);
}
function normalizeRelative(value) {
return value.split(path.sep).join('/');
}
function isAllowedFile(relativePath) {
return allowedFiles.has(relativePath) || allowedPrefixes.some((prefix) => relativePath.startsWith(prefix));
}
function isDeniedBasename(basename) {
const lower = basename.toLowerCase();
return deniedBasenames.has(lower)
|| /^\.env(?:\.|$)/i.test(basename)
|| /\.(?:bak|db|log|sqlite|sqlite3|tmp)$/i.test(basename);
}
async function enumerate(directory = root, relativeDirectory = '') {
const entries = await readdir(directory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const relativePath = normalizeRelative(path.join(relativeDirectory, entry.name));
const lowerName = entry.name.toLowerCase();
if (entry.isDirectory()) {
if (ignoredDirectories.has(lowerName)) continue;
if (deniedDirectories.has(lowerName)) addFailure(relativePath, 'denied-directory');
files.push(...await enumerate(path.join(directory, entry.name), relativePath));
continue;
}
if (!entry.isFile()) {
addFailure(relativePath, 'unsupported-file-type');
continue;
}
if (isDeniedBasename(entry.name)) addFailure(relativePath, 'denied-basename');
if (!isAllowedFile(relativePath)) addFailure(relativePath, 'source-layout-allowlist');
files.push(relativePath);
}
return files;
}
async function readJson(relativePath, rule) {
try {
return JSON.parse(await readFile(path.join(root, relativePath), 'utf8'));
} catch {
addFailure(relativePath, rule);
return null;
}
}
function stableJson(value) {
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
if (value && typeof value === 'object') {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
}
return JSON.stringify(value);
}
async function validateTextFiles(files) {
for (const relativePath of files) {
const extension = path.extname(relativePath).toLowerCase();
const isTextFile = textExtensions.has(extension) || relativePath === '.gitignore';
if (!isTextFile) {
if (!binaryExtensions.has(extension)) addFailure(relativePath, 'source-extension-allowlist');
continue;
}
const text = await readFile(path.join(root, relativePath), 'utf8');
if (forbiddenPersonalPattern.test(text)) addFailure(relativePath, 'forbidden-personal-term');
if (forbiddenAiPattern.test(text)) addFailure(relativePath, 'forbidden-ai-term');
if (forbiddenInvestigationPattern.test(text)) addFailure(relativePath, 'forbidden-investigation-term');
if (relativePath !== 'lib/updater.js' && updaterOnlyPattern.test(text)) addFailure(relativePath, 'updater-endpoint-scope');
}
}
function validatePackage(packageJson, packageLock, files) {
if (!packageJson) return;
if (packageJson.version !== '3.3.108') addFailure('package.json', 'package-version');
if (stableJson(packageJson.scripts) !== stableJson(expectedScripts)) addFailure('package.json', 'package-script-allowlist');
const buildFiles = packageJson.build?.files;
if (!Array.isArray(buildFiles) || stableJson(buildFiles) !== stableJson(expectedBuildFiles)) {
addFailure('package.json', 'build-file-allowlist');
}
for (const requiredEntry of expectedBuildFiles) {
if (!Array.isArray(buildFiles) || !buildFiles.includes(requiredEntry)) addFailure(requiredEntry.replace('/**/*', ''), 'build-file-entry');
}
if (packageJson.build?.afterPack !== 'scripts/afterPack.cjs') addFailure('scripts/afterPack.cjs', 'build-hook-entry');
if (packageLock) {
const lockRoot = packageLock.packages?.[''];
if (packageLock.version !== '3.3.108' || lockRoot?.version !== '3.3.108') {
addFailure('package-lock.json', 'package-lock-version');
}
if (!lockRoot
|| lockRoot.name !== packageJson.name
|| stableJson(lockRoot.dependencies) !== stableJson(packageJson.dependencies)
|| stableJson(lockRoot.devDependencies) !== stableJson(packageJson.devDependencies)) {
addFailure('package-lock.json', 'package-lock-root-metadata');
}
}
for (const entry of expectedBuildFiles) {
if (entry.endsWith('/**/*')) {
const prefix = entry.slice(0, -4);
if (!files.some((file) => file.startsWith(prefix))) addFailure(entry.slice(0, -5), 'build-entry-target');
} else if (!files.includes(entry)) {
addFailure(entry, 'build-entry-target');
}
}
}
function printFailures() {
for (const file of [...failures.keys()].sort()) {
for (const rule of [...failures.get(file)].sort()) {
process.stderr.write(`${file}\t${rule}\n`);
}
}
}
async function main() {
if (args.some((arg) => arg !== '--source-only') || args.filter((arg) => arg === '--source-only').length > 1) {
addFailure('scripts/verify-public-release.mjs', 'argument-allowlist');
}
const files = await enumerate();
for (const requiredFile of requiredFiles) {
if (!files.includes(requiredFile)) addFailure(requiredFile, 'required-source-file');
}
if (!sourceOnly && !files.includes('assets/product-overview.png')) {
addFailure('assets/product-overview.png', 'required-screenshot');
}
await validateTextFiles(files);
const packageJson = await readJson('package.json', 'package-json');
const packageLock = await readJson('package-lock.json', 'package-lock-json');
validatePackage(packageJson, packageLock, files);
if (failures.size > 0) {
printFailures();
process.exitCode = 1;
return;
}
process.stdout.write(`public-release-source-ok files=${files.length} denied-paths=0 forbidden-terms=0 version=${packageJson.version} scripts=${Object.keys(packageJson.scripts).length} build-files=${packageJson.build.files.length} layout=valid screenshot=${sourceOnly ? 'deferred' : 'present'}\n`);
}
main().catch(() => {
process.stderr.write('scripts/verify-public-release.mjs\tverifier-runtime\n');
process.exitCode = 1;
});