Redesign the desktop workspace with task sidebars, live filters, clearer settings, and an accessible update dialog. Harden encrypted backup imports, configuration persistence, history retention, queue snapshots, shutdown recovery, and update installation ordering. Publish verified Windows artifacts and refreshed English documentation.
405 lines
14 KiB
JavaScript
405 lines
14 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 sourceFiles = [
|
|
'.gitignore',
|
|
'README.md',
|
|
'SECURITY.md',
|
|
'assets/app_icon.ico',
|
|
'assets/app_icon.png',
|
|
'eslint.config.mjs',
|
|
'lib/account-auth.js',
|
|
'lib/account-rotation.js',
|
|
'lib/backup-crypto.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/folder-monitor.js',
|
|
'lib/hosters.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/startup-renderer.js',
|
|
'lib/stats.js',
|
|
'lib/support-bundle.js',
|
|
'lib/throttle-timer.js',
|
|
'lib/throttle.js',
|
|
'lib/throttled-cache.js',
|
|
'lib/updater.js',
|
|
'lib/upload-log.js',
|
|
'lib/upload-manager.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/drop-target.html',
|
|
'renderer/index.html',
|
|
'renderer/styles.css',
|
|
'scripts/afterPack.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/backup-crypto.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/doodstream-api-upload.test.js',
|
|
'tests/doodstream-upload.test.js',
|
|
'tests/file-probe.test.js',
|
|
'tests/history-retention.test.js',
|
|
'tests/hosters.test.js',
|
|
'tests/ip-allowlist.test.js',
|
|
'tests/log-mode.test.js',
|
|
'tests/log-policy.test.js',
|
|
'tests/log-rotation.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/remote-config.test.js',
|
|
'tests/remote-server.test.js',
|
|
'tests/semaphore.test.js',
|
|
'tests/serialized-runner.test.js',
|
|
'tests/settings-backup.test.js',
|
|
'tests/settings-import-gate.test.js',
|
|
'tests/startup-renderer.test.js',
|
|
'tests/stats.test.js',
|
|
'tests/support-bundle.test.js',
|
|
'tests/suspect-reject-alternates.test.js',
|
|
'tests/throttle-timer.test.js',
|
|
'tests/throttle.test.js',
|
|
'tests/throttled-cache.test.js',
|
|
'tests/ui-smoke.js',
|
|
'tests/updater-version.test.js',
|
|
'tests/upload-log.test.js',
|
|
'tests/upload-manager.test.js',
|
|
'tests/validate-credentials.test.js',
|
|
'tests/webhook-notify.test.js'
|
|
];
|
|
const screenshotFile = 'assets/product-overview.png';
|
|
const allowedFiles = new Set([...sourceFiles, screenshotFile]);
|
|
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',
|
|
'test:backup-api': 'npm --prefix services/backup-api test',
|
|
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 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 validateScreenshot(sourceOnly) {
|
|
if (sourceOnly) return;
|
|
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, screenshotFile];
|
|
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 validateScreenshot(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;
|
|
});
|