UNPKG

@zozzona/js

Version:

Advanced source protection toolkit (obfuscate → minify → encrypt) with reversible unpacking and git-safe automation.

562 lines (459 loc) 15.2 kB
#!/usr/bin/env node // --------------------------------------------------------------- // FORCE TTY MODE (spinner support under npm / husky) // --------------------------------------------------------------- if (!process.stdout.isTTY) process.stdout.isTTY = true; import { spawn } from "child_process"; import fs from "fs-extra"; import path from "path"; import { sync as globSync } from "glob"; import dotenv from "dotenv"; import { encryptFileSync, decryptFileSync } from "./zozzonaUtils.js"; import { loadPackConfig } from "./config.js"; import { fileURLToPath } from "url"; // --------------------------------------------------------------- // Resolve THIS package root + safe obfuscation plugin // --------------------------------------------------------------- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Safe obfuscation plugin that preserves imports/exports/Node globals const OBFUSCATE_PLUGIN = path.join(__dirname, "babel-obfuscate-plugin.cjs"); dotenv.config(); if (!process.env.MAP_KEY) { console.error("❌ Missing MAP_KEY in .env"); process.exit(1); } const PACK_CONFIG = loadPackConfig(); // =============================================================== // AUTO-FIX PACKAGE.JSON SCRIPTS (critical fix for deminify) // =============================================================== function ensureCorrectScripts() { const pkgPath = path.resolve("package.json"); if (!fs.existsSync(pkgPath)) return; let pkg; try { pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); } catch { console.warn("⚠ package.json unreadable — skipping script repair"); return; } pkg.scripts = pkg.scripts || {}; const expected = { obfuscate: "node node_modules/@zozzona/js/src/obfuscate.js obfuscate", deobfuscate: "node node_modules/@zozzona/js/src/obfuscate.js deobfuscate", minify: "node node_modules/@zozzona/js/src/minify.js minify", deminify: "node node_modules/@zozzona/js/src/minify.js restore" }; let changed = false; for (const [script, cmd] of Object.entries(expected)) { if (pkg.scripts[script] !== cmd) { pkg.scripts[script] = cmd; changed = true; } } if (changed) { fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); console.log("🔧 Auto-fixed zozzona scripts in package.json"); } } // Run immediately ensureCorrectScripts(); // =============================================================== // SHARED HELPER: run external commands (npm scripts) // =============================================================== async function run(cmd, args = [], env = {}) { return new Promise((resolve, reject) => { const p = spawn(cmd, args, { stdio: "inherit", shell: true, env: { ...process.env, ...env } }); p.on("close", code => (code === 0 ? resolve() : reject(code))); }); } // =============================================================== // ORIGINAL REVERSIBLE SOURCE PIPELINE (pack / unpack, JS only) // =============================================================== // Build **only JS-related** include patterns function buildIncludePatterns() { const patterns = []; for (const folder of PACK_CONFIG.folders) { patterns.push(`${folder}/**/*.{js,jsx,ts,tsx}`); patterns.push(folder); // ensure folder is always included } for (const file of PACK_CONFIG.files) { if (/\.(js|jsx|ts|tsx)$/.test(file)) { patterns.push(file); } else { console.warn(`⚠ Skipping non-JS file in pack.config.json files[]: ${file}`); } } return patterns; } function buildIgnorePatterns() { const ignore = [ "**/*.json", "**/*.css", "**/package.json", "**/package-lock.json", "**/*.map", "**/node_modules/**" ]; for (const ig of PACK_CONFIG.ignore) { ignore.push(ig, `${ig}/**`); } return ignore; } function buildGlobEnv() { return { PACK_INCLUDE: buildIncludePatterns().join("|"), // Use | instead of , to avoid breaking {js,jsx,ts,tsx} PACK_IGNORE: buildIgnorePatterns().join("|") }; } // =============================================================== // MAP FILES // =============================================================== const MAP_FILES = [ "minify-map.json", "terser-name-cache.json", "obfuscation-map.json", "json-minify-map.json", "css-minify-map.json" ]; async function remove(file) { if (await fs.pathExists(file)) await fs.remove(file); } async function clearMaps() { for (const f of MAP_FILES) { await remove(f); await remove(f + ".enc"); } for (const f of globSync("**/*.map")) await remove(f); for (const f of globSync("**/*.map.enc")) await remove(f); } function encryptMaps() { for (const f of MAP_FILES) { if (fs.existsSync(f)) { encryptFileSync(f); fs.removeSync(f); } } for (const f of globSync("**/*.map")) { encryptFileSync(f); fs.removeSync(f); } } function decryptMaps() { let error = false; for (const f of MAP_FILES) { const enc = f + ".enc"; if (fs.existsSync(enc)) { try { decryptFileSync(enc, f); } catch { error = true; } } } for (const f of globSync("**/*.map.enc")) { try { decryptFileSync(f, f.replace(".enc", "")); } catch { error = true; } } if (error) { console.error("❌ Decryption failed. Bad MAP_KEY?"); process.exit(1); } } // =============================================================== // JSON + CSS reversible minification // =============================================================== function getJsonAndCssSourceFiles() { const ignore = [ "**/node_modules/**", "**/dist/**", "**/.git/**" ]; for (const ig of PACK_CONFIG.ignore) { ignore.push(ig, `${ig}/**`); } const jsonFiles = []; const cssFiles = []; for (const folder of PACK_CONFIG.folders) { const base = folder.replace(/\/+$/, ""); jsonFiles.push(...globSync(`${base}/**/*.json`, { ignore })); cssFiles.push(...globSync(`${base}/**/*.css`, { ignore })); } return { jsonFiles, cssFiles }; } function minifyCssString(source) { return source .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/\s+/g, " ") .replace(/\s*([{}:;,])\s*/g, "$1") .replace(/;}/g, "}"); } async function reversibleMinifyJsonAndCss() { const { jsonFiles, cssFiles } = getJsonAndCssSourceFiles(); const jsonMap = {}; const cssMap = {}; for (const file of jsonFiles) { try { const original = fs.readFileSync(file, "utf8"); jsonMap[file] = original; const parsed = JSON.parse(original); fs.writeFileSync(file, JSON.stringify(parsed), "utf8"); console.log("Minified JSON:", file); } catch (err) { console.warn("⚠ Invalid JSON:", file); } } for (const file of cssFiles) { try { const original = fs.readFileSync(file, "utf8"); cssMap[file] = original; fs.writeFileSync(file, minifyCssString(original), "utf8"); console.log("Minified CSS:", file); } catch (err) { console.warn("⚠ CSS error:", file); } } if (Object.keys(jsonMap).length) fs.writeFileSync("json-minify-map.json", JSON.stringify(jsonMap), "utf8"); if (Object.keys(cssMap).length) fs.writeFileSync("css-minify-map.json", JSON.stringify(cssMap), "utf8"); } function restoreJsonAndCssFromMaps() { if (fs.existsSync("json-minify-map.json")) { const map = JSON.parse(fs.readFileSync("json-minify-map.json", "utf8")); for (const [file, original] of Object.entries(map)) { if (fs.existsSync(file)) { fs.writeFileSync(file, original, "utf8"); console.log("Restored JSON:", file); } } } if (fs.existsSync("css-minify-map.json")) { const map = JSON.parse(fs.readFileSync("css-minify-map.json", "utf8")); for (const [file, original] of Object.entries(map)) { if (fs.existsSync(file)) { fs.writeFileSync(file, original, "utf8"); console.log("Restored CSS:", file); } } } } // =============================================================== // BOM STRIPPER - Run before any processing // =============================================================== // IMPROVED: Strip BOM from both string and buffer level function stripBOM(str) { if (!str) return str; // Remove UTF-8 BOM (U+FEFF) if (str.charCodeAt(0) === 0xFEFF) { return str.slice(1); } // Also handle the string representation if (str.startsWith('\uFEFF')) { return str.slice(1); } // Also strip the literal UTF-8 BOM bytes if they somehow got in if (str.startsWith('')) { return str.slice(3); } return str; } function stripBOMFromFile(filePath) { try { // Read as buffer first to see if BOM exists at byte level const buffer = fs.readFileSync(filePath); let hadBOM = false; // Check for UTF-8 BOM at byte level (EF BB BF) if (buffer.length >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { // Strip BOM at byte level const cleanBuffer = buffer.slice(3); fs.writeFileSync(filePath, cleanBuffer); hadBOM = true; } else { // Also try string-level BOM removal as fallback const content = buffer.toString('utf8'); const stripped = stripBOM(content); if (content !== stripped) { fs.writeFileSync(filePath, stripped, 'utf8'); hadBOM = true; } } if (hadBOM) { console.log(`🧹 Stripped BOM from: ${filePath}`); return true; } } catch (err) { // File doesn't exist or can't be read, skip } return false; } function stripAllBOMs() { console.log("🧹 Checking for BOMs in source files..."); let stripped = 0; // Strip from ALL package.json files (be very aggressive) const packageJsonFiles = globSync("**/package.json", { ignore: ["**/node_modules/**"], absolute: true }); console.log(` Found ${packageJsonFiles.length} package.json files to check`); for (const file of packageJsonFiles) { if (stripBOMFromFile(file)) stripped++; } // Also explicitly check server/package.json if (fs.existsSync("server/package.json")) { if (stripBOMFromFile("server/package.json")) stripped++; } // Strip from all JS/TS files in configured folders for (const folder of PACK_CONFIG.folders) { const files = globSync(`${folder}/**/*.{js,jsx,ts,tsx,json}`, { ignore: ["**/node_modules/**", "**/dist/**"] }); for (const file of files) { if (stripBOMFromFile(file)) stripped++; } } if (stripped > 0) { console.log(`✅ Stripped BOMs from ${stripped} file(s)`); } else { console.log(` No BOMs found`); } } // =============================================================== // PACK PIPELINE // =============================================================== async function runPackPipeline() { console.log("🔒 Running PACK (source)…"); // Strip BOMs FIRST before any processing stripAllBOMs(); const env = buildGlobEnv(); await clearMaps(); await run("npm", ["run", "obfuscate"], env); // Strip BOMs AGAIN after obfuscation (in case any were introduced) stripAllBOMs(); await run("npm", ["run", "minify"], env); await reversibleMinifyJsonAndCss(); encryptMaps(); console.log("✔ Pack complete"); } // =============================================================== // UNPACK PIPELINE // =============================================================== async function runUnpackPipeline() { console.log("🔓 Running UNPACK (source)…"); const env = buildGlobEnv(); decryptMaps(); restoreJsonAndCssFromMaps(); await run("npm", ["run", "deminify"], env); await run("npm", ["run", "deobfuscate"], env); await clearMaps(); console.log("✔ Unpack complete"); } // =============================================================== // DIST PIPELINE // =============================================================== const DIST_ROOT = "server/client/dist"; function getDistJSFiles() { return globSync(`${DIST_ROOT}/**/*.js`).filter(f => !f.endsWith(".enc")); } function getDistJSONFiles() { return globSync(`${DIST_ROOT}/**/*.json`).filter(f => !f.endsWith(".enc")); } function getDistMapFiles() { return globSync(`${DIST_ROOT}/**/*.map`).filter(f => !f.endsWith(".enc")); } async function obfuscateDist(jsFiles) { const babel = (await import("@babel/core")).default; for (const file of jsFiles) { const start = Date.now(); console.log(`\n⚙️ Obfuscating (dist): ${file}`); const frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]; let i = 0; let spinning = true; const spinner = setInterval(() => { if (!spinning) return; process.stdout.write(`\r${frames[i++ % frames.length]} Working... `); }, 80); try { const src = fs.readFileSync(file, "utf8"); const { code } = await babel.transformAsync(src, { plugins: [[OBFUSCATE_PLUGIN, {}]], sourceMaps: false }); fs.writeFileSync(file, code, "utf8"); } finally { spinning = false; clearInterval(spinner); process.stdout.write("\r \r"); const secs = ((Date.now() - start) / 1000).toFixed(2); console.log(`✔ Obfuscated in ${secs}s`); } } } async function minifyDist(jsFiles) { // FIXED: Named import instead of default const { minify } = await import("terser"); for (const file of jsFiles) { const code = fs.readFileSync(file, "utf8"); const minified = await minify(babelCode, { compress: false, // ← DISABLE ALL COMPRESSION mangle: true, // Keep name mangling format: { semicolons: true, beautify: false } }); fs.writeFileSync(file, result.code, "utf8"); console.log("Minified (dist):", file); } } function minifyJSONFiles(jsonFiles) { for (const file of jsonFiles) { try { const parsed = JSON.parse(fs.readFileSync(file, "utf8")); fs.writeFileSync(file, JSON.stringify(parsed), "utf8"); console.log("Minified JSON (dist):", file); } catch {} } } function encryptDistMaps(mapFiles) { for (const f of mapFiles) { encryptFileSync(f); fs.removeSync(f); console.log("Encrypted map (dist):", f); } } async function runDistPipeline() { console.log("📦 Running dist packing pipeline…"); const js = getDistJSFiles(); const json = getDistJSONFiles(); const maps = getDistMapFiles(); if (js.length === 0 && json.length === 0) { console.log("ℹ No dist files found."); return; } await obfuscateDist(js); await minifyDist(js); minifyJSONFiles(json); encryptDistMaps(maps); console.log("✔ dist pack complete"); } // =============================================================== // MAIN // =============================================================== (async () => { const cmd = process.argv[2]; if (cmd === "pack:dist") return runDistPipeline(); if (cmd === "pack") return runPackPipeline(); if (cmd === "unpack") return runUnpackPipeline(); console.log("Usage: runner.js [pack|unpack|pack:dist]"); })();