Preserve uncertain remote commits and destructive source-cleanup requirements across cancellation, persistence failures, retries, and restart recovery. Revalidate account availability after admission waits and prevent late upload results from bypassing cancellation state. Strengthen release argument, CI tag, source allowlist, and secret scanning gates. Run the Electron smoke suite in a fully hidden offscreen harness that cannot reveal, focus, or elevate native windows.
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
const PRODUCT_NAME = 'Multi Hoster Uploader';
|
||||
const ARTIFACT_NAME = 'Multi-Hoster-Upload';
|
||||
const NON_ENGLISH_RELEASE_PATTERN = /\b(?:das|dies(?:e|er|es)|fehler|für|hinzugefügt|jetzt|korrigiert|laufen|mit|nach|probleme|programm|sicherheit|stabil|und|verbessert|verbesserungen|werden|wieder|wurde|wurden|zuverlässig\w*|amélior\w*|après|avec|corrig\w*|et|les|mises|pour|reprennent|réseau|sécurité|téléversements)\b/iu;
|
||||
const ENGLISH_RELEASE_PATTERN = /\b(?:added|after|and|are|bridge|cancel|changes|credential|files?|faster|fixed|hardened|improved|installation|more|new|notes?|now|recovery|release|reliable|resume|security|safer|smoother|source|the|updates?|uploads?|with|without)\b/i;
|
||||
|
||||
function requireEnglishReleaseNotes(value) {
|
||||
const notes = typeof value === 'string' ? value.trim() : '';
|
||||
if (!/[A-Za-z]/.test(notes)) {
|
||||
const words = notes.match(/[A-Za-z]+(?:['’-][A-Za-z]+)*/g) || [];
|
||||
if (words.length < 2 || /(?![A-Za-z])\p{L}/u.test(notes) || NON_ENGLISH_RELEASE_PATTERN.test(notes) || !ENGLISH_RELEASE_PATTERN.test(notes)) {
|
||||
throw new Error('English release notes are required');
|
||||
}
|
||||
return notes;
|
||||
@@ -12,18 +15,58 @@ function requireEnglishReleaseNotes(value) {
|
||||
export function parseReleaseArgs(args) {
|
||||
const version = Array.isArray(args) ? args[0] : '';
|
||||
if (!/^\d+\.\d+\.\d+$/.test(version || '')) {
|
||||
throw new Error('Usage: <version> --transport-tag <vX.Y.Z> [release notes] [--dry-run]');
|
||||
throw new Error('Usage: <version> --transport-tag <vX.Y.Z> --notes <English release notes> [--dry-run]');
|
||||
}
|
||||
|
||||
let transportTag = '';
|
||||
let dryRun = false;
|
||||
let notes = '';
|
||||
const legacyNoteParts = [];
|
||||
for (let index = 1; index < args.length; index++) {
|
||||
const arg = args[index];
|
||||
if (arg === '--transport-tag') {
|
||||
if (transportTag) {
|
||||
throw new Error('Duplicate option: --transport-tag');
|
||||
}
|
||||
transportTag = args[++index] || '';
|
||||
if (!/^v\d+\.\d+\.\d+$/.test(transportTag)) {
|
||||
throw new Error('--transport-tag must match vX.Y.Z');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === '--notes') {
|
||||
if (notes) {
|
||||
throw new Error('Duplicate option: --notes');
|
||||
}
|
||||
const value = args[++index];
|
||||
if (typeof value !== 'string' || !value.trim() || value.startsWith('-')) {
|
||||
throw new Error('--notes requires English release notes');
|
||||
}
|
||||
notes = value;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--dry-run') {
|
||||
if (dryRun) {
|
||||
throw new Error('Duplicate option: --dry-run');
|
||||
}
|
||||
dryRun = true;
|
||||
continue;
|
||||
}
|
||||
if (typeof arg !== 'string' || arg.startsWith('-')) {
|
||||
throw new Error(`Unknown option: ${String(arg)}`);
|
||||
}
|
||||
if (notes) {
|
||||
throw new Error(`Unexpected release note argument: ${arg}`);
|
||||
}
|
||||
legacyNoteParts.push(arg);
|
||||
}
|
||||
|
||||
const transportTagIndex = args.indexOf('--transport-tag');
|
||||
const transportTag = transportTagIndex >= 0 ? args[transportTagIndex + 1] : '';
|
||||
if (!/^v\d+\.\d+\.\d+$/.test(transportTag)) {
|
||||
throw new Error('--transport-tag must match vX.Y.Z');
|
||||
}
|
||||
|
||||
const excludedIndexes = new Set([0, transportTagIndex, transportTagIndex + 1]);
|
||||
const notes = requireEnglishReleaseNotes(args.filter((arg, index) => !excludedIndexes.has(index) && arg !== '--dry-run').join(' '));
|
||||
return { version, transportTag, notes, dryRun: args.includes('--dry-run') };
|
||||
const releaseNotes = requireEnglishReleaseNotes(notes || legacyNoteParts.join(' '));
|
||||
return { version, transportTag, notes: releaseNotes, dryRun };
|
||||
}
|
||||
|
||||
export function createReleasePlan(options) {
|
||||
@@ -53,6 +96,10 @@ export function createReleasePlan(options) {
|
||||
|
||||
export function resolveExistingReleaseId(plan, release) {
|
||||
const existingTitle = typeof release?.name === 'string' ? release.name : '';
|
||||
const existingTag = typeof release?.tag_name === 'string' ? release.tag_name : '';
|
||||
if (existingTag !== plan.tag) {
|
||||
throw new Error(`Refusing recovery for ${plan.tag}: existing release tag "${existingTag}" does not match "${plan.tag}"`);
|
||||
}
|
||||
if (existingTitle !== plan.releaseTitle) {
|
||||
throw new Error(`Refusing recovery for ${plan.tag}: existing release title "${existingTitle}" does not match "${plan.releaseTitle}"`);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ const sourceFiles = [
|
||||
'tests/folder-monitor.test.js',
|
||||
'tests/history-status.test.js',
|
||||
'tests/history-retention.test.js',
|
||||
'tests/hidden-electron-window.test.js',
|
||||
'tests/hosters.test.js',
|
||||
'tests/hoster-recovery-provenance.test.js',
|
||||
'tests/hoster-recovery-safety.test.js',
|
||||
@@ -151,6 +152,7 @@ const sourceFiles = [
|
||||
'tests/startup-renderer.test.js',
|
||||
'tests/stats.test.js',
|
||||
'tests/support-bundle.test.js',
|
||||
'tests/support/hidden-electron-window.js',
|
||||
'tests/support/ui-network-safety.js',
|
||||
'tests/suspect-reject-alternates.test.js',
|
||||
'tests/throttle-timer.test.js',
|
||||
@@ -163,6 +165,7 @@ const sourceFiles = [
|
||||
'tests/upload-log.test.js',
|
||||
'tests/upload-confirmation.test.js',
|
||||
'tests/upload-diagnostics.test.js',
|
||||
'tests/upload-finalization-coordinator.test.js',
|
||||
'tests/upload-manager.test.js',
|
||||
'tests/upload-manager-recovery-claims.test.js',
|
||||
'tests/upload-recovery.test.js',
|
||||
@@ -236,11 +239,22 @@ const internalTerms = [
|
||||
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');
|
||||
const credentialPatterns = [
|
||||
new RegExp(`\\b${['gh', '[pousr]_'].join('')}[A-Za-z0-9]{36,}\\b`),
|
||||
/\bgithub_pat_[A-Za-z0-9_]{60,}\b/,
|
||||
/\bnpm_[A-Za-z0-9]{36,}\b/,
|
||||
/\bxox[baprs]-[A-Za-z0-9-]{24,}\b/,
|
||||
/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
|
||||
/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}(?=$|[\s"'`,;}\]])/i,
|
||||
new RegExp(['-----BEGIN ', '(?:RSA |EC |DSA |OPENSSH )?', 'PRIVATE KEY-----'].join('')),
|
||||
new RegExp(`\\b${['aws', '_secret_access_key'].join('')}\\b\\s*[:=]\\s*["']?[A-Za-z0-9+/]{40}["']?`, 'i')
|
||||
];
|
||||
const credentialAssignmentPattern = /\b(?:password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|token|secret|cookie|session(?:id|token)?|authorization)\b\s*[:=]\s*["'`]([^"'`\r\n]{8,})["'`]/giu;
|
||||
const unquotedCredentialAssignmentPattern = /\b(?:password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|token|secret|cookie|session(?:id|token)?|authorization)\b\s*[:=]\s*([A-Za-z0-9._~+/=-]{12,})\s*(?:#.*)?$/gimu;
|
||||
const updaterOnlyTerms = [
|
||||
'ed.cisum-42.tig'.split('').reverse().join('').replace(/[^a-z0-9]/g, ''),
|
||||
'daolpu-retsoh-itlum/rotartsinimda'.split('').reverse().join('').replace(/[^a-z0-9]/g, '')
|
||||
];
|
||||
|
||||
function addFailure(file, rule) {
|
||||
if (!failures.has(file)) failures.set(file, new Set());
|
||||
@@ -353,8 +367,15 @@ async function enumerateTracked() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = stdout.split('\0').filter(Boolean).map(normalizeRelative);
|
||||
for (const relativePath of files) {
|
||||
const files = new Set(stdout.split('\0').filter(Boolean).map(normalizeRelative));
|
||||
const buildDirectories = ['lib', 'renderer'];
|
||||
for (const relativeDirectory of buildDirectories) {
|
||||
const buildFiles = await enumerateBuildDirectory(path.join(root, relativeDirectory), relativeDirectory);
|
||||
for (const relativePath of buildFiles) files.add(relativePath);
|
||||
}
|
||||
|
||||
const result = [...files];
|
||||
for (const relativePath of result) {
|
||||
let stats;
|
||||
try {
|
||||
stats = await lstat(path.join(root, relativePath));
|
||||
@@ -366,6 +387,33 @@ async function enumerateTracked() {
|
||||
if (isDeniedBasename(path.basename(relativePath))) addFailure(relativePath, 'denied-basename');
|
||||
if (!allowedFiles.has(relativePath)) addFailure(relativePath, 'source-layout-allowlist');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function enumerateBuildDirectory(directory, relativeDirectory) {
|
||||
const files = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(directory, { withFileTypes: true });
|
||||
} catch {
|
||||
addFailure(relativeDirectory, 'build-input-enumeration');
|
||||
return files;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = normalizeRelative(path.join(relativeDirectory, entry.name));
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
const stats = await lstat(absolutePath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
addFailure(relativePath, 'unsupported-file-type');
|
||||
} else if (entry.isDirectory()) {
|
||||
files.push(...await enumerateBuildDirectory(absolutePath, relativePath));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(relativePath);
|
||||
} else {
|
||||
addFailure(relativePath, 'unsupported-file-type');
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -399,10 +447,41 @@ async function validateTextFiles(files) {
|
||||
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');
|
||||
credentialAssignmentPattern.lastIndex = 0;
|
||||
unquotedCredentialAssignmentPattern.lastIndex = 0;
|
||||
const credentialAssignment = [...value.matchAll(credentialAssignmentPattern)]
|
||||
.some((match) => !isCredentialPlaceholder(match[1], relativePath))
|
||||
|| [...value.matchAll(unquotedCredentialAssignmentPattern)]
|
||||
.some((match) => !isCredentialPlaceholder(match[1], relativePath));
|
||||
if (credentialPatterns.some((pattern) => pattern.test(value)) || credentialAssignment) {
|
||||
addFailure(relativePath, 'credential-pattern');
|
||||
}
|
||||
const normalizedValue = value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
if (relativePath !== 'lib/updater.js' && updaterOnlyTerms.some((term) => normalizedValue.includes(term))) {
|
||||
addFailure(relativePath, 'updater-endpoint-scope');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isCredentialPlaceholder(value, relativePath) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (!normalized
|
||||
|| /^(?:\[?redacted\]?|none|null|undefined)$/i.test(normalized)
|
||||
|| /(?:dummy|example|fake|invalid|mock|notreal|placeholder|synthetic|test)[-_ ]?/i.test(normalized)
|
||||
|| /\$\{[^}]+\}/.test(normalized)
|
||||
|| /^<[^>]+>$/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (!relativePath.startsWith('tests/')) return false;
|
||||
const longHex = /^[a-f0-9]{32,}$/i.test(normalized);
|
||||
const opaqueLetters = /^[a-z]{16,}$/i.test(normalized);
|
||||
const highEntropy = normalized.length >= 24
|
||||
&& /[a-z]/.test(normalized)
|
||||
&& /\d/.test(normalized)
|
||||
&& !/[-_ ](?:key|token|secret|password|session|account|fixture)(?:[-_ ]|$)/i.test(normalized);
|
||||
return !longHex && !opaqueLetters && !highEntropy;
|
||||
}
|
||||
|
||||
function validatePackage(packageJson, packageLock, files, expectedVersion) {
|
||||
if (!packageJson) return;
|
||||
if (packageJson.version !== expectedVersion) addFailure('package.json', 'package-version-target');
|
||||
|
||||
Reference in New Issue
Block a user