const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const AdmZip = require('adm-zip'); const { execSync } = require('child_process'); const BUILD_DIR = path.join(__dirname, 'dist-build'); const REPOS = ['frontend', 'player', 'auth-proxy', 'stream-api']; const TEMP_EXTRACT_DIR = path.join(__dirname, 'temp-validation-extract'); const GITLAB_GROUP = 'vyla-entertainment'; const REPO_CONFIG = { 'frontend': { name: 'frontend', branch: 'main' }, 'player': { name: 'player', branch: 'main' }, 'auth-proxy': { name: 'auth-proxy', branch: 'main' }, 'stream-api': { name: 'stream-api', branch: 'main' }, }; const clonedRepos = new Set(); function clonePrivateRepo(repoConfig, targetDir) { if (fs.existsSync(targetDir)) { console.log(`Removing stale ${repoConfig.name} before fresh clone...`); fs.rmSync(targetDir, { recursive: true, force: true }); } const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN; const auth = token ? `oauth2:${token}@` : ''; const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`; try { execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, { stdio: 'inherit' }); clonedRepos.add(repoConfig.name); } catch (error) { throw new Error(`Failed to clone ${repoConfig.name}: ${error.message}`); } } function cloneAllRepos() { for (const [key, config] of Object.entries(REPO_CONFIG)) { const targetDir = path.join(__dirname, key); clonePrivateRepo(config, targetDir); } } async function installDependencies() { await Promise.all(REPOS.map(repo => installOneRepo(repo))); } function installOneRepo(repo) { const repoDir = path.join(__dirname, repo); const pkgJsonPath = path.join(repoDir, 'package.json'); if (!fs.existsSync(pkgJsonPath)) { return; } try { execSync('npm install --omit=dev', { cwd: repoDir, stdio: 'inherit' }); } catch (error) { throw new Error(`npm install failed for ${repo}: ${error.message}`); } } function cleanBuildDir() { if (fs.existsSync(BUILD_DIR)) { fs.rmSync(BUILD_DIR, { recursive: true, force: true }); } fs.mkdirSync(BUILD_DIR, { recursive: true }); } function encryptCredentialsFile() { const CryptoUtils = require('./crypto-utils'); const credentialsTemplate = path.join(__dirname, 'config/credentials-template.json'); const encryptedOutput = path.join(__dirname, 'config/encrypted-credentials.json'); if (!fs.existsSync(credentialsTemplate)) { return null; } try { const credentials = JSON.parse(fs.readFileSync(credentialsTemplate, 'utf8')); const hasPlaceholders = Object.values(credentials).some(service => Object.values(service).some(value => value && value.includes('YOUR_') && value.includes('_HERE') ) ); if (hasPlaceholders) { const envCredentials = {}; for (const [service, keys] of Object.entries(credentials)) { envCredentials[service] = {}; for (const key of Object.keys(keys)) { const envVarName = `${service.toUpperCase().replace(/-/g, '_')}_${key.toUpperCase()}`; if (process.env[envVarName]) { envCredentials[service][key] = process.env[envVarName]; } } } if (Object.values(envCredentials).some(s => Object.keys(s).length > 0)) { Object.assign(credentials, envCredentials); } } const buildKey = CryptoUtils.deriveKey('vyla-home-build-key', 'vyla-home-payload-salt-v1'); const encryptedData = CryptoUtils.encryptObject(credentials, buildKey); fs.writeFileSync(encryptedOutput, encryptedData, 'utf8'); return encryptedOutput; } catch (error) { throw error; } } function collectJsFiles(src, dest, out) { const files = fs.readdirSync(src); fs.mkdirSync(dest, { recursive: true }); for (const file of files) { const srcPath = path.join(src, file); const destPath = path.join(dest, file); const stat = fs.statSync(srcPath); if (stat.isDirectory()) { if (file === 'node_modules' || file === '.git' || file === '.github') continue; collectJsFiles(srcPath, destPath, out); } else if (stat.isFile()) { fs.copyFileSync(srcPath, destPath); if (path.extname(srcPath).toLowerCase() === '.js') { out.push(destPath); } } } } function processFolder(src, dest) { const jsFiles = []; collectJsFiles(src, dest, jsFiles); let isEsm = false; const pkgPath = path.join(src, 'package.json'); if (fs.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); if (pkg.type === 'module') isEsm = true; } catch (e) { } } } function getFileChecksum(filePath) { const hash = crypto.createHash('sha256'); const data = fs.readFileSync(filePath); hash.update(data); return hash.digest('hex'); } function validatePayload(zipPath) { if (fs.existsSync(TEMP_EXTRACT_DIR)) { fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true }); } fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true }); const zip = new AdmZip(zipPath); zip.extractAllTo(TEMP_EXTRACT_DIR, true); const requiredFiles = [ 'player/index.html', 'auth-proxy/server.js', 'stream-api/server.js', 'frontend/index.html' ]; const validationResults = { valid: true, errors: [], checksum: getFileChecksum(zipPath), timestamp: new Date().toISOString() }; for (const file of requiredFiles) { const fullPath = path.join(TEMP_EXTRACT_DIR, file); if (!fs.existsSync(fullPath)) { validationResults.valid = false; validationResults.errors.push(`Missing critical file: ${file}`); } } const envPath = path.join(__dirname, 'config/env.json'); const replacementsPath = path.join(__dirname, 'config/replacements.json'); if (!fs.existsSync(envPath)) { validationResults.valid = false; validationResults.errors.push('Missing environment configuration: config/env.json'); } else { try { JSON.parse(fs.readFileSync(envPath, 'utf8')); } catch (e) { validationResults.valid = false; validationResults.errors.push(`Invalid JSON format in config/env.json: ${e.message}`); } } if (!fs.existsSync(replacementsPath)) { validationResults.valid = false; validationResults.errors.push('Missing replacements configuration: config/replacements.json'); } else { try { JSON.parse(fs.readFileSync(replacementsPath, 'utf8')); } catch (e) { validationResults.valid = false; validationResults.errors.push(`Invalid JSON format in config/replacements.json: ${e.message}`); } } fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true }); fs.writeFileSync(path.join(__dirname, 'validation-report.json'), JSON.stringify(validationResults, null, 2), 'utf8'); if (!validationResults.valid) { console.error("Payload validation failed with errors:", validationResults.errors); process.exit(1); } return validationResults.checksum; } async function build() { try { cloneAllRepos(); } catch (error) { console.error("Cloning failed:", error); process.exit(1); } for (const repo of REPOS) { const srcRepo = path.join(__dirname, repo); if (!fs.existsSync(srcRepo)) { console.error(`Repository directory missing: ${srcRepo}`); process.exit(1); } } try { await installDependencies(); } catch (error) { console.error("Dependency installation failed:", error); process.exit(1); } let encryptedCredentialsPath = null; try { encryptedCredentialsPath = encryptCredentialsFile(); } catch (error) { console.error("Credential encryption warning (non-fatal):", error); } cleanBuildDir(); for (const repo of REPOS) { const srcRepo = path.join(__dirname, repo); const destRepo = path.join(BUILD_DIR, repo); processFolder(srcRepo, destRepo); const srcModules = path.join(srcRepo, 'node_modules'); const destModules = path.join(destRepo, 'node_modules'); if (fs.existsSync(srcModules)) { fs.cpSync(srcModules, destModules, { recursive: true, filter: (src) => !/[\\/](test|tests|docs|\.github|\.git)([\\/]|$)/.test(src) && !/\.(md|markdown|map|ts)$/i.test(src) }); } } if (encryptedCredentialsPath && fs.existsSync(encryptedCredentialsPath)) { const credentialsDest = path.join(BUILD_DIR, 'encrypted-credentials.json'); fs.copyFileSync(encryptedCredentialsPath, credentialsDest); } const configDir = path.join(BUILD_DIR, 'config'); fs.mkdirSync(configDir, { recursive: true }); const envConfig = path.join(__dirname, 'config/env.json'); const replacementsConfig = path.join(__dirname, 'config/replacements.json'); if (fs.existsSync(envConfig)) { fs.copyFileSync(envConfig, path.join(configDir, 'env.json')); } if (fs.existsSync(replacementsConfig)) { fs.copyFileSync(replacementsConfig, path.join(configDir, 'replacements.json')); } const zip = new AdmZip(); for (const repo of REPOS) { const destRepo = path.join(BUILD_DIR, repo); zip.addLocalFolder(destRepo, repo); } zip.addLocalFolder(configDir, 'config'); if (fs.existsSync(path.join(BUILD_DIR, 'encrypted-credentials.json'))) { zip.addFile('encrypted-credentials.json', fs.readFileSync(path.join(BUILD_DIR, 'encrypted-credentials.json'))); } const zipPath = path.join(__dirname, 'update-payload.zip'); if (fs.existsSync(zipPath)) { fs.rmSync(zipPath, { force: true }); } zip.writeZip(zipPath); const checksum = validatePayload(zipPath); let wrapperVersion = '2.0.0'; try { const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')); wrapperVersion = pkg.version || '2.0.0'; } catch (e) { } const manifest = { version: wrapperVersion, sha256: checksum, timestamp: new Date().toISOString(), hasEncryptedCredentials: !!encryptedCredentialsPath }; fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8'); for (const repo of REPOS) { if (clonedRepos.has(repo)) { const repoPath = path.join(__dirname, repo); if (fs.existsSync(repoPath)) { fs.rmSync(repoPath, { recursive: true, force: true }); } } } } build().catch((error) => { console.error("Build process encountered an unhandled exception:", error); process.exit(1); });