50 lines
3.4 KiB
JavaScript
50 lines
3.4 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const desktopDir = path.resolve(new URL('..', import.meta.url).pathname);
|
|
const repoDir = path.resolve(desktopDir, '..', '..');
|
|
const webDir = path.join(repoDir, 'apps', 'web');
|
|
const requiredDesktop = ['package.json', 'main.cjs', 'preload.cjs', 'url-validation.js', 'connection.html', 'desktop-manifest.json'];
|
|
const secretAssignment = /(?:api[_-]?key|secret|token|password|private[_-]?key)\s*[:=]\s*['"][^'"\n]{8,}['"]/i;
|
|
const fail = (message) => { throw new Error(message); };
|
|
async function filesUnder(dir) {
|
|
const output = [];
|
|
async function walk(current) {
|
|
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'release') continue;
|
|
const full = path.join(current, entry.name);
|
|
if (entry.isDirectory()) await walk(full); else output.push(full);
|
|
}
|
|
}
|
|
await walk(dir); return output.sort();
|
|
}
|
|
const digest = (buffer) => `sha256-${createHash('sha256').update(buffer).digest('hex')}`;
|
|
for (const file of requiredDesktop) await stat(path.join(desktopDir, file)).catch(() => fail(`Missing desktop entrypoint: ${file}`));
|
|
const webManifest = JSON.parse(await readFile(path.join(webDir, 'asset-manifest.json'), 'utf8'));
|
|
if (webManifest.schema !== 1 || !webManifest.version || !webManifest.integrity) fail('Invalid apps/web asset manifest');
|
|
const listed = [...new Set([...(webManifest.entrypoints || []), ...(webManifest.publicAssets || [])])].sort();
|
|
if (!listed.includes('index.html')) fail('Web manifest must include index.html');
|
|
for (const [relative, expected] of Object.entries(webManifest.integrity)) {
|
|
const full = path.join(webDir, relative);
|
|
await stat(full).catch(() => fail(`Manifest asset is missing: ${relative}`));
|
|
const actual = digest(await readFile(full));
|
|
if (actual !== expected) fail(`Integrity mismatch for ${relative}`);
|
|
}
|
|
for (const relative of listed) if (!webManifest.integrity[relative]) fail(`Manifest asset lacks integrity: ${relative}`);
|
|
const desktopManifest = JSON.parse(await readFile(path.join(desktopDir, 'desktop-manifest.json'), 'utf8'));
|
|
for (const relative of desktopManifest.assets || []) await stat(path.resolve(desktopDir, relative)).catch(() => fail(`Desktop manifest asset is missing: ${relative}`));
|
|
const files = [...await filesUnder(webDir), ...await filesUnder(desktopDir)];
|
|
for (const file of files) {
|
|
const buffer = await readFile(file);
|
|
if (buffer.includes(0)) continue;
|
|
if (secretAssignment.test(buffer.toString('utf8'))) fail(`Secret-like assignment found in ${path.relative(repoDir, file)}`);
|
|
}
|
|
const packageJson = JSON.parse(await readFile(path.join(desktopDir, 'package.json'), 'utf8'));
|
|
if (packageJson.main !== 'main.cjs') fail('package.json main must be main.cjs');
|
|
if (!packageJson.build?.extraResources?.some((item) => item.from === '../web' && item.to === 'web')) fail('Build must copy apps/web into packaged resources');
|
|
const targets = packageJson.build?.win?.target || [];
|
|
if (!targets.some((target) => target.target === 'nsis')) fail('Windows NSIS target is missing');
|
|
if (!targets.some((target) => target.target === 'portable')) fail('Windows portable target is missing');
|
|
console.log(`Packaging verification passed: ${listed.length} web assets, ${files.length} scanned source files, NSIS + portable targets.`);
|