Files
Home/build-pipeline.js

366 lines
10 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const JavaScriptObfuscator = require('javascript-obfuscator');
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' },
};
function clonePrivateRepo(repoConfig, targetDir) {
const repoUrl = `https://gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
try {
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true });
}
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, {
stdio: 'inherit'
});
} 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 obfuscateFile(filePath, isEsm = false) {
const content = fs.readFileSync(filePath, 'utf8');
const result = JavaScriptObfuscator.obfuscate(content, {
compact: true,
controlFlowFlattening: false,
deadCodeInjection: false,
debugProtection: false,
numbersToExpressions: false,
simplify: true,
stringArray: true,
stringArrayThreshold: 0.4,
stringArrayEncoding: [],
module: isEsm
});
fs.writeFileSync(filePath, result.getObfuscatedCode(), 'utf8');
}
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) { }
}
for (const filePath of jsFiles) {
try {
obfuscateFile(filePath, isEsm);
} 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');
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) {
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);
});