const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const AdmZip = require('adm-zip'); const CryptoUtils = require('./crypto-utils'); class Patcher { constructor(appDataPath) { this.appDataPath = appDataPath; } getCurrentVersion() { const versionFile = path.join(this.appDataPath, 'version.json'); if (fs.existsSync(versionFile)) { try { return JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '0.0.0'; } catch (e) { return '0.0.0'; } } return '0.0.0'; } getCurrentSha() { const versionFile = path.join(this.appDataPath, 'version.json'); if (fs.existsSync(versionFile)) { try { return JSON.parse(fs.readFileSync(versionFile, 'utf8')).sha256 || null; } catch (e) { return null; } } return null; } bridgeCredentials(credentialManager) { const encryptedPath = path.join(this.appDataPath, 'encrypted-credentials.json'); if (!fs.existsSync(encryptedPath)) { return false; } const buildKey = CryptoUtils.deriveKey('vyla-home-build-key', 'vyla-home-payload-salt-v1'); const encryptedData = fs.readFileSync(encryptedPath, 'utf8'); const credentials = CryptoUtils.decryptObject(encryptedData, buildKey); for (const [service, keys] of Object.entries(credentials)) { credentialManager.setServiceCredentials(service, keys); } fs.unlinkSync(encryptedPath); return true; } saveCurrentVersion(version, sha256 = null) { const versionFile = path.join(this.appDataPath, 'version.json'); fs.mkdirSync(this.appDataPath, { recursive: true }); fs.writeFileSync(versionFile, JSON.stringify({ version, sha256 }, null, 2), 'utf8'); } purgeOldCode() { const dirs = ['frontend', 'player', 'stream-api', 'auth-proxy', 'config']; for (const dir of dirs) { const fullPath = path.join(this.appDataPath, dir); if (fs.existsSync(fullPath)) { try { fs.rmSync(fullPath, { recursive: true, force: true }); } catch (e) { try { const trashPath = fullPath + '.trash.' + Date.now(); fs.renameSync(fullPath, trashPath); } catch (renameErr) { console.warn(`[PATCHER] Could not remove or rename ${fullPath}. It will be overwritten.`); } } } } } extractZipPayload(zipPath) { const zip = new AdmZip(zipPath); zip.extractAllTo(this.appDataPath, true); } verifyChecksum(zipPath, expectedSha256) { if (!expectedSha256) return true; const hash = crypto.createHash('sha256'); const data = fs.readFileSync(zipPath); hash.update(data); const calculated = hash.digest('hex'); return calculated === expectedSha256; } verifyExtraction() { const requiredFiles = [ 'player/index.html', 'auth-proxy/server.js', 'stream-api/server.js', 'frontend/index.html' ]; for (const file of requiredFiles) { const fullPath = path.join(this.appDataPath, file); if (!fs.existsSync(fullPath)) { return false; } const stat = fs.statSync(fullPath); if (stat.size === 0) { return false; } const sampleSize = Math.min(stat.size, 4096); const fd = fs.openSync(fullPath, 'r'); const buffer = Buffer.alloc(sampleSize); fs.readSync(fd, buffer, 0, sampleSize, 0); fs.closeSync(fd); if (buffer.every(byte => byte === 0)) { return false; } } return true; } processDirectory(dir, replacementList) { const files = fs.readdirSync(dir); for (const file of files) { const fullPath = path.join(dir, file); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { if (file === 'node_modules' || file === '.git') continue; this.processDirectory(fullPath, replacementList); } else if (stat.isFile()) { const ext = path.extname(fullPath).toLowerCase(); if (['.js', '.html', '.css', '.json', '.xml', '.txt'].includes(ext)) { let content = fs.readFileSync(fullPath, 'utf8'); let modified = false; for (const rep of replacementList) { if (content.includes(rep.find)) { content = content.split(rep.find).join(rep.replace); modified = true; } } if (modified) { fs.writeFileSync(fullPath, content, 'utf8'); } } } } } applyPatches(replacements) { for (const [componentName, replacementList] of Object.entries(replacements)) { if (!replacementList || replacementList.length === 0) continue; const componentDir = path.join(this.appDataPath, componentName); if (!fs.existsSync(componentDir)) continue; this.processDirectory(componentDir, replacementList); } } } module.exports = Patcher;