Allow the committed remote input regression test in the positive-list source verifier. The verifier still rejects every unlisted path and now accepts the complete v2.1.20 source snapshot without broadening any directory or extension rules.
464 lines
16 KiB
JavaScript
464 lines
16 KiB
JavaScript
import { lstat, 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 failures = new Map();
|
|
const publicActionsDir = `.${['git', 'hub'].join('')}`;
|
|
const privateActionsDir = `.${['gi', 'tea'].join('')}`;
|
|
const sourceFiles = [
|
|
'.gitignore',
|
|
'README.md',
|
|
'SECURITY.md',
|
|
'assets/app_icon.ico',
|
|
'assets/app_icon.png',
|
|
'eslint.config.mjs',
|
|
`${privateActionsDir}/workflows/ci.yml`,
|
|
`${publicActionsDir}/workflows/ci.yml`,
|
|
'lib/account-auth.js',
|
|
'lib/account-rotation.js',
|
|
'lib/backup-crypto.js',
|
|
'lib/batch-mutation-gate.js',
|
|
'lib/clouddrop-upload.js',
|
|
'lib/coalesced-set.js',
|
|
'lib/config-store.js',
|
|
'lib/diagnostics-agent.js',
|
|
'lib/diagnostics-collectors.js',
|
|
'lib/doodstream-upload.js',
|
|
'lib/file-probe.js',
|
|
'lib/file-discovery.js',
|
|
'lib/folder-monitor.js',
|
|
'lib/hosters.js',
|
|
'lib/hoster-transport-error.js',
|
|
'lib/ip-allowlist.js',
|
|
'lib/log-mode.js',
|
|
'lib/log-policy.js',
|
|
'lib/log-rotation.js',
|
|
'lib/online-backup.js',
|
|
'lib/orphan-tmp.js',
|
|
'lib/queue-dedup.js',
|
|
'lib/queue-prune.js',
|
|
'lib/remote-capture-preload.js',
|
|
'lib/remote-capture.html',
|
|
'lib/remote-server.js',
|
|
'lib/secret-store.js',
|
|
'lib/semaphore.js',
|
|
'lib/serialized-runner.js',
|
|
'lib/settings-backup.js',
|
|
'lib/settings-import-gate.js',
|
|
'lib/source-cleanup-policy.js',
|
|
'lib/source-delete-journal.js',
|
|
'lib/source-file-cleanup.js',
|
|
'lib/speed-history.js',
|
|
'lib/startup-renderer.js',
|
|
'lib/stats.js',
|
|
'lib/support-bundle.js',
|
|
'lib/session-report.js',
|
|
'lib/throttle-timer.js',
|
|
'lib/throttle.js',
|
|
'lib/throttled-cache.js',
|
|
'lib/updater.js',
|
|
'lib/upload-audit.js',
|
|
'lib/upload-log.js',
|
|
'lib/upload-confirmation.js',
|
|
'lib/upload-diagnostics.js',
|
|
'lib/upload-manager.js',
|
|
'lib/upload-recovery.js',
|
|
'lib/upload-start-reservation.js',
|
|
'lib/vidmoly-upload.js',
|
|
'lib/voe-upload.js',
|
|
'lib/webhook-notify.js',
|
|
'main.js',
|
|
'package-lock.json',
|
|
'package.json',
|
|
'preload-drop-target.js',
|
|
'preload.js',
|
|
'renderer/account-status.js',
|
|
'renderer/account-submit.js',
|
|
'renderer/app.js',
|
|
'renderer/auto-resume.js',
|
|
'renderer/drop-target.html',
|
|
'renderer/i18n.js',
|
|
'renderer/index.html',
|
|
'renderer/history-status.js',
|
|
'renderer/queue-stats.js',
|
|
'renderer/styles.css',
|
|
'scripts/afterPack.cjs',
|
|
'scripts/dev-runner.cjs',
|
|
'scripts/release-plan.mjs',
|
|
'scripts/verify-public-release.mjs',
|
|
'services/backup-api/package-lock.json',
|
|
'services/backup-api/package.json',
|
|
'services/backup-api/src/cli.mjs',
|
|
'services/backup-api/src/server.mjs',
|
|
'services/backup-api/test/server.test.mjs',
|
|
'tests/account-auth.test.js',
|
|
'tests/account-rotation.test.js',
|
|
'tests/account-status.test.js',
|
|
'tests/auto-resume.test.js',
|
|
'tests/backup-crypto.test.js',
|
|
'tests/batch-mutation-gate.test.js',
|
|
'tests/byse-reject-recovery.test.js',
|
|
'tests/coalesced-set.test.js',
|
|
'tests/config-store.test.js',
|
|
'tests/diagnostics-agent.test.js',
|
|
'tests/diagnostics-collectors.test.js',
|
|
'tests/diagnostics-protocol.test.js',
|
|
'tests/dev-runner.test.js',
|
|
'tests/doodstream-api-upload.test.js',
|
|
'tests/doodstream-upload.test.js',
|
|
'tests/file-probe.test.js',
|
|
'tests/file-discovery.test.js',
|
|
'tests/folder-monitor.test.js',
|
|
'tests/history-status.test.js',
|
|
'tests/history-retention.test.js',
|
|
'tests/hosters.test.js',
|
|
'tests/hoster-recovery-provenance.test.js',
|
|
'tests/hoster-recovery-safety.test.js',
|
|
'tests/i18n.test.js',
|
|
'tests/ip-allowlist.test.js',
|
|
'tests/log-mode.test.js',
|
|
'tests/log-policy.test.js',
|
|
'tests/log-rotation.test.js',
|
|
'tests/main-remote-input.test.js',
|
|
'tests/online-backup-service.test.js',
|
|
'tests/online-backup.test.js',
|
|
'tests/orphan-tmp.test.js',
|
|
'tests/package-build-files.test.js',
|
|
'tests/public-release-verifier.test.js',
|
|
'tests/queue-dedup-property.test.js',
|
|
'tests/queue-dedup.test.js',
|
|
'tests/queue-persistence-scenario.test.js',
|
|
'tests/queue-prune.test.js',
|
|
'tests/queue-stats.test.js',
|
|
'tests/remote-config.test.js',
|
|
'tests/remote-server.test.js',
|
|
'tests/semaphore.test.js',
|
|
'tests/secret-store.test.js',
|
|
'tests/serialized-runner.test.js',
|
|
'tests/settings-backup.test.js',
|
|
'tests/settings-import-gate.test.js',
|
|
'tests/speed-history.test.js',
|
|
'tests/source-cleanup-policy.test.js',
|
|
'tests/source-delete-journal.test.js',
|
|
'tests/source-file-cleanup.test.js',
|
|
'tests/startup-renderer.test.js',
|
|
'tests/stats.test.js',
|
|
'tests/support-bundle.test.js',
|
|
'tests/support/ui-network-safety.js',
|
|
'tests/suspect-reject-alternates.test.js',
|
|
'tests/throttle-timer.test.js',
|
|
'tests/throttle.test.js',
|
|
'tests/throttled-cache.test.js',
|
|
'tests/ui-network-safety.test.js',
|
|
'tests/ui-smoke.js',
|
|
'tests/updater-version.test.js',
|
|
'tests/upload-audit.test.js',
|
|
'tests/upload-log.test.js',
|
|
'tests/upload-confirmation.test.js',
|
|
'tests/upload-diagnostics.test.js',
|
|
'tests/upload-manager.test.js',
|
|
'tests/upload-manager-recovery-claims.test.js',
|
|
'tests/upload-recovery.test.js',
|
|
'tests/upload-start-reservation.test.js',
|
|
'tests/session-report.test.js',
|
|
'tests/validate-credentials.test.js',
|
|
'tests/webhook-notify.test.js'
|
|
];
|
|
const screenshotFiles = [
|
|
'assets/product-overview.png',
|
|
'docs/screenshots/upload-workspace.png',
|
|
'docs/screenshots/account-management.png',
|
|
'docs/screenshots/automation-settings.png',
|
|
'docs/screenshots/history.png'
|
|
];
|
|
const allowedFiles = new Set([...sourceFiles, ...screenshotFiles]);
|
|
const textExtensions = new Set(['.cjs', '.css', '.html', '.js', '.json', '.md', '.mjs', '.txt', '.yaml', '.yml']);
|
|
const binaryExtensions = new Set(['.ico', '.png']);
|
|
const expectedScripts = {
|
|
start: 'electron .',
|
|
dev: 'node scripts/dev-runner.cjs',
|
|
test: 'npm run test:unit && npm run test:ui',
|
|
'test:unit': 'node --test tests/*.test.js',
|
|
'test:ui': 'node --test tests/ui-smoke.js',
|
|
'test:backup-api': 'npm --prefix services/backup-api test',
|
|
verify: 'npm run lint && npm test && npm run test:backup-api && npm audit --omit=dev',
|
|
lint: 'eslint .',
|
|
dist: 'electron-builder --publish never --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 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',
|
|
['release_', ['gi', 'tea'].join(''), '.mjs'].join('')
|
|
]);
|
|
const aiTerms = [
|
|
['clau', 'de'].join(''),
|
|
['co', 'dex'].join(''),
|
|
['chat', 'gpt'].join('')
|
|
].join('|');
|
|
const personalTerms = [
|
|
['pl', 'oet'].join(''),
|
|
['baker', 'edwin318'].join('')
|
|
].join('|');
|
|
const internalTerms = [
|
|
['internal', ' investigation'].join(''),
|
|
['interne', ' untersuchung'].join(''),
|
|
['audit', ' method'].join(''),
|
|
['test', ' chronicle'].join(''),
|
|
['generated', ' by'].join(''),
|
|
['co-authored', '-by'].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 forbiddenInternalPattern = new RegExp(`\\b(?:${internalTerms})\\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 buildAllowedDirectories(files) {
|
|
const directories = new Set();
|
|
for (const file of files) {
|
|
let current = path.posix.dirname(file);
|
|
while (current && current !== '.') {
|
|
directories.add(current);
|
|
current = path.posix.dirname(current);
|
|
}
|
|
}
|
|
return directories;
|
|
}
|
|
|
|
const allowedDirectories = buildAllowedDirectories(allowedFiles);
|
|
|
|
function isDeniedBasename(basename) {
|
|
const lower = basename.toLowerCase();
|
|
return deniedBasenames.has(lower)
|
|
|| /^\.env(?:\.|$)/i.test(basename)
|
|
|| /\.(?:bak|db|log|sqlite|sqlite3|tmp)$/i.test(basename);
|
|
}
|
|
|
|
function parseArguments() {
|
|
const sourceOnlyCount = args.filter((arg) => arg === '--source-only').length;
|
|
const versionFlagIndexes = args.map((arg, index) => arg === '--version' ? index : -1).filter((index) => index >= 0);
|
|
const versionIndex = versionFlagIndexes[0] ?? -1;
|
|
const expectedVersion = versionIndex >= 0 ? args[versionIndex + 1] : '';
|
|
const consumed = new Set();
|
|
|
|
if (sourceOnlyCount === 1) consumed.add(args.indexOf('--source-only'));
|
|
if (sourceOnlyCount > 1) addFailure('scripts/verify-public-release.mjs', 'duplicate-source-only');
|
|
if (versionFlagIndexes.length !== 1 || !/^\d+\.\d+\.\d+$/.test(expectedVersion || '')) {
|
|
addFailure('scripts/verify-public-release.mjs', 'expected-version-argument');
|
|
} else {
|
|
consumed.add(versionIndex);
|
|
consumed.add(versionIndex + 1);
|
|
}
|
|
|
|
for (let index = 0; index < args.length; index++) {
|
|
if (!consumed.has(index)) addFailure('scripts/verify-public-release.mjs', 'argument-allowlist');
|
|
}
|
|
|
|
return { sourceOnly: sourceOnlyCount === 1, expectedVersion };
|
|
}
|
|
|
|
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));
|
|
if (relativePath === '.git') continue;
|
|
const absolutePath = path.join(directory, entry.name);
|
|
const stats = await lstat(absolutePath);
|
|
|
|
if (stats.isSymbolicLink()) {
|
|
addFailure(relativePath, 'unsupported-file-type');
|
|
continue;
|
|
}
|
|
|
|
if (entry.isDirectory()) {
|
|
if (!allowedDirectories.has(relativePath)) {
|
|
addFailure(relativePath, 'source-layout-allowlist');
|
|
continue;
|
|
}
|
|
files.push(...await enumerate(absolutePath, relativePath));
|
|
continue;
|
|
}
|
|
|
|
if (!entry.isFile()) {
|
|
addFailure(relativePath, 'unsupported-file-type');
|
|
continue;
|
|
}
|
|
|
|
if (isDeniedBasename(entry.name)) addFailure(relativePath, 'denied-basename');
|
|
if (!allowedFiles.has(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 value = await readFile(path.join(root, relativePath), 'utf8');
|
|
if (forbiddenPersonalPattern.test(value)) addFailure(relativePath, 'forbidden-personal-term');
|
|
if (forbiddenAiPattern.test(value)) addFailure(relativePath, 'forbidden-ai-term');
|
|
if (forbiddenInternalPattern.test(value)) addFailure(relativePath, 'forbidden-internal-term');
|
|
if (relativePath !== 'lib/updater.js' && updaterOnlyPattern.test(value)) addFailure(relativePath, 'updater-endpoint-scope');
|
|
}
|
|
}
|
|
|
|
function validatePackage(packageJson, packageLock, files, expectedVersion) {
|
|
if (!packageJson) return;
|
|
if (packageJson.version !== expectedVersion) addFailure('package.json', 'package-version-target');
|
|
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');
|
|
}
|
|
if (packageJson.build?.afterPack !== 'scripts/afterPack.cjs') addFailure('scripts/afterPack.cjs', 'build-hook-entry');
|
|
|
|
if (packageLock) {
|
|
const lockRoot = packageLock.packages?.[''];
|
|
if (packageLock.version !== expectedVersion || lockRoot?.version !== expectedVersion) {
|
|
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 validateServicePackage(packageJson, packageLock) {
|
|
if (!packageJson || !packageLock) return;
|
|
const lockRoot = packageLock.packages?.[''];
|
|
if (packageLock.version !== packageJson.version || lockRoot?.version !== packageJson.version) {
|
|
addFailure('services/backup-api/package-lock.json', 'service-lock-version');
|
|
}
|
|
if (!lockRoot || lockRoot.name !== packageJson.name || stableJson(lockRoot.dependencies) !== stableJson(packageJson.dependencies)) {
|
|
addFailure('services/backup-api/package-lock.json', 'service-lock-root-metadata');
|
|
}
|
|
}
|
|
|
|
async function validateScreenshots(sourceOnly) {
|
|
if (sourceOnly) return;
|
|
for (const screenshotFile of screenshotFiles) {
|
|
try {
|
|
const data = await readFile(path.join(root, screenshotFile));
|
|
const signature = data.subarray(0, 8).toString('hex');
|
|
const width = data.length >= 24 ? data.readUInt32BE(16) : 0;
|
|
const height = data.length >= 24 ? data.readUInt32BE(20) : 0;
|
|
if (signature !== '89504e470d0a1a0a' || width < 1000 || height < 650) {
|
|
addFailure(screenshotFile, 'product-screenshot');
|
|
}
|
|
} catch {
|
|
addFailure(screenshotFile, 'required-screenshot');
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
const { sourceOnly, expectedVersion } = parseArguments();
|
|
const files = await enumerate();
|
|
const requiredFiles = sourceOnly ? sourceFiles : [...sourceFiles, ...screenshotFiles];
|
|
for (const requiredFile of requiredFiles) {
|
|
if (!files.includes(requiredFile)) addFailure(requiredFile, 'required-source-file');
|
|
}
|
|
|
|
await validateTextFiles(files);
|
|
const packageJson = await readJson('package.json', 'package-json');
|
|
const packageLock = await readJson('package-lock.json', 'package-lock-json');
|
|
const servicePackage = await readJson('services/backup-api/package.json', 'service-package-json');
|
|
const serviceLock = await readJson('services/backup-api/package-lock.json', 'service-package-lock-json');
|
|
validatePackage(packageJson, packageLock, files, expectedVersion);
|
|
validateServicePackage(servicePackage, serviceLock);
|
|
await validateScreenshots(sourceOnly);
|
|
|
|
if (failures.size > 0) {
|
|
printFailures();
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
process.stdout.write(`public-release-source-ok files=${files.length} denied-paths=0 internal-terms=0 version=${packageJson.version} scripts=${Object.keys(packageJson.scripts).length} build-files=${packageJson.build.files.length} layout=exact screenshot=${sourceOnly ? 'deferred' : 'valid'}\n`);
|
|
}
|
|
|
|
main().catch(() => {
|
|
process.stderr.write('scripts/verify-public-release.mjs\tverifier-runtime\n');
|
|
process.exitCode = 1;
|
|
});
|