From 775d3b2b42181a8e51643bc60df80a5f2031fcce Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 03:20:47 +0000 Subject: [PATCH] Upload files to "scripts" --- scripts/build-cli.ts | 181 +++++++++++++++++++++++++++++++++++++++ scripts/build-plugin.ts | 82 ++++++++++++++++++ scripts/build.ts | 183 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 scripts/build-cli.ts create mode 100644 scripts/build-plugin.ts create mode 100644 scripts/build.ts diff --git a/scripts/build-cli.ts b/scripts/build-cli.ts new file mode 100644 index 0000000..22e6382 --- /dev/null +++ b/scripts/build-cli.ts @@ -0,0 +1,181 @@ +import { promises as fs } from "fs"; +import * as path from "path"; + +import { transformEnvAccess } from "./env-scramble"; +import { + generateBuildPassphrase, + generateBuildSalt, + generateFunctionName, + rewriteRuntimeDecoder, + RUNTIME_DECODER_PATH, + transformSource, +} from "./scramble-shared"; +import { stripLogCalls } from "./strip-logs"; + +(globalThis as any).scramble = (s: string) => s; + +const { StringScrambler } = await import("../src/utils/stringtool"); + +const PASSPHRASE = generateBuildPassphrase(); +const SALT = generateBuildSalt(); +const FN_NAME = generateFunctionName(); +console.log( + `[BUILD-CLI] Generated build passphrase (${PASSPHRASE.length} chars)`, +); +console.log(`[BUILD-CLI] Generated build salt (${SALT.length} chars)`); +console.log(`[BUILD-CLI] Generated function name: ${FN_NAME}`); + +const scrambler = new StringScrambler(PASSPHRASE, SALT); + +const loggerSource = await fs.readFile("src/utils/logger.ts", "utf-8"); +const isSilent = /const\s+isSilent\s*=\s*true/.test(loggerSource); +console.log(`[BUILD-CLI] isSilent = ${isSilent}`); + +const CLI_DIR = "src/cli"; +const OUT_DIR = "dist-cli"; + +const BEAUTIFY_IDENTITY = /^\(globalThis as Record\)\.beautify\s*=\s*\(s:\s*string\):\s*string\s*=>\s*s;?\s*\n?/m; +const SCRAMBLE_IDENTITY = /^\(globalThis as Record\)\.scramble\s*=\s*\(s:\s*string\):\s*string\s*=>\s*s;?\s*\n?/m; + +const SHEBANG = /^#!.*\n/; + +function stripCLIGlobals(code: string): string { + return code + .replace(SHEBANG, "") + .replace(SCRAMBLE_IDENTITY, "") + .replace(BEAUTIFY_IDENTITY, ""); +} + +async function transformFile(filePath: string): Promise { + const code = await fs.readFile(filePath, "utf-8"); + + console.log(`[TRANSFORM] Processing: ${filePath}`); + + const { code: envRewritten } = transformEnvAccess( + code, + "[TRANSFORM]", + filePath, + ); + + const { code: scrambled } = transformSource( + envRewritten, + scrambler, + FN_NAME, + "[TRANSFORM]", + filePath, + ); + + if (isSilent) { + const { code: stripped } = stripLogCalls( + scrambled, + "[TRANSFORM]", + filePath, + ); + return stripped; + } + + return scrambled; +} + +async function walkDir(dir: string): Promise { + const files: string[] = []; + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await walkDir(fullPath))); + } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) { + files.push(fullPath); + } + } + + return files; +} + +async function buildCLI() { + const tempDir = "./.bun-temp-cli"; + await fs.rm(tempDir, { recursive: true, force: true }); + await fs.mkdir(tempDir, { recursive: true }); + + console.log("[BUILD-CLI] Copying and transforming source files..."); + + const files = await walkDir("./src"); + console.log(`[BUILD-CLI] Processing ${files.length} TypeScript files`); + + for (const file of files) { + let transformed = await transformFile(file); + + // Strip identity scramble/beautify from CLI entrypoints — the built + // bundle uses runtimeDecoder instead. + if (file.startsWith(CLI_DIR + "/")) { + transformed = stripCLIGlobals(transformed); + } + + const tempFile = path.join(tempDir, path.relative("./src", file)); + await fs.mkdir(path.dirname(tempFile), { recursive: true }); + await fs.writeFile(tempFile, transformed, "utf-8"); + } + + const tempDecoderPath = path.join( + tempDir, + path.relative("./src", path.resolve(RUNTIME_DECODER_PATH)), + ); + const rewrittenDecoder = await rewriteRuntimeDecoder( + RUNTIME_DECODER_PATH, + PASSPHRASE, + SALT, + FN_NAME, + ); + await fs.mkdir(path.dirname(tempDecoderPath), { recursive: true }); + await fs.writeFile(tempDecoderPath, rewrittenDecoder, "utf-8"); + console.log( + `[BUILD-CLI] Injected build passphrase into ${path.relative(tempDir, tempDecoderPath)}`, + ); + + const cliTempDir = path.join(tempDir, "cli"); + const cliEntries = (await fs.readdir(cliTempDir)) + .filter((f) => f.endsWith(".ts") && f !== "common.ts") + .map((f) => path.join(cliTempDir, f)); + + console.log(`[BUILD-CLI] Found ${cliEntries.length} CLI entrypoints`); + + // Prepend runtimeDecoder import (relative from src/cli/ → src/utils/) + for (const ep of cliEntries) { + const epCode = await fs.readFile(ep, "utf-8"); + await fs.writeFile( + ep, + `import "../utils/runtimeDecoder";\n${epCode}`, + "utf-8", + ); + } + + const allEntrypoints = [...cliEntries]; + + console.log( + `[BUILD-CLI] Building ${allEntrypoints.length} entrypoints...`, + ); + + await Bun.build({ + entrypoints: allEntrypoints, + outdir: OUT_DIR, + root: tempDir, + naming: { + entry: "[dir]/[name].[ext]", + }, + target: "bun", + minify: true, + }); + + console.log("[BUILD-CLI] Cleaning up temp directory..."); + await fs.rm(tempDir, { recursive: true, force: true }); + + console.log("[BUILD-CLI] ✓ Build complete!"); + console.log("[BUILD-CLI]"); + for (const ep of cliEntries) { + const name = path.basename(ep, ".ts"); + console.log(`[BUILD-CLI] dist-cli/cli/${name}.js`); + } +} + +buildCLI().catch(console.error); \ No newline at end of file diff --git a/scripts/build-plugin.ts b/scripts/build-plugin.ts new file mode 100644 index 0000000..ba2fd39 --- /dev/null +++ b/scripts/build-plugin.ts @@ -0,0 +1,82 @@ +import { plugin } from "bun"; + +import { + generateBuildPassphrase, + RUNTIME_PASSPHRASE_PLACEHOLDER, + transformSource, +} from "./scramble-shared"; + +// Provide a global identity stub for scramble() so that un-transformed +// source modules can be safely evaluated when we import StringScrambler +// from the source tree. +(globalThis as any).scramble = (s: string) => s; + +// Dynamic import — MUST come after the stub is installed. +const { StringScrambler } = await import("../src/utils/stringtool"); + +const PASSPHRASE = generateBuildPassphrase(); +console.log( + `[SCRAMBLE] Generated build passphrase (${PASSPHRASE.length} chars)`, +); + +const scrambler = new StringScrambler(PASSPHRASE); + +const RUNTIME_DECODER_FILE_REGEX = /[\\/]src[\\/]utils[\\/]runtimeDecoder\.ts$/; +const ENTRYPOINT_FILE_REGEX = /[\\/]src[\\/]index\.ts$/; +const QUOTED_PLACEHOLDER = `"${RUNTIME_PASSPHRASE_PLACEHOLDER}"`; + +plugin({ + name: "scramble", + setup(build) { + build.onLoad({ filter: /\.ts$/ }, async (args) => { + let code = await Bun.file(args.path).text(); + + if (RUNTIME_DECODER_FILE_REGEX.test(args.path)) { + if (!code.includes(QUOTED_PLACEHOLDER)) { + throw new Error( + `[SCRAMBLE] runtime decoder ${args.path} does not contain the ` + + `expected placeholder ${QUOTED_PLACEHOLDER}. The build ` + + `pipeline cannot inject a passphrase, which would lead to ` + + `garbled strings at runtime.`, + ); + } + + const literal = JSON.stringify(PASSPHRASE); + code = code.split(QUOTED_PLACEHOLDER).join(literal); + console.log(`[SCRAMBLE] Injected build passphrase into ${args.path}`); + + return { + contents: code, + loader: "ts", + }; + } + + if (ENTRYPOINT_FILE_REGEX.test(args.path)) { + console.log( + `[SCRAMBLE] Prepending runtime decoder import to ${args.path}`, + ); + code = `import "./utils/runtimeDecoder";\n${code}`; + } + + console.log(`[SCRAMBLE] Processing: ${args.path}`); + + const { code: transformed, replacements } = transformSource( + code, + scrambler, + "[SCRAMBLE]", + args.path, + ); + + if (replacements > 0) { + console.log( + `[SCRAMBLE] Encoded ${replacements} call(s) in ${args.path}`, + ); + } + + return { + contents: transformed, + loader: "ts", + }; + }); + }, +}); diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..02a04ad --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,183 @@ +import { promises as fs } from "fs"; +import * as path from "path"; + +import { transformEnvAccess } from "./env-scramble"; +import { + generateBuildPassphrase, + generateBuildSalt, + generateFunctionName, + rewriteRuntimeDecoder, + RUNTIME_DECODER_PATH, + transformSource, +} from "./scramble-shared"; +import { stripLogCalls } from "./strip-logs"; + +(globalThis as any).scramble = (s: string) => s; + +const { StringScrambler } = await import("../src/utils/stringtool"); + +const PASSPHRASE = generateBuildPassphrase(); +const SALT = generateBuildSalt(); +const FN_NAME = generateFunctionName(); +console.log(`[BUILD] Generated build passphrase (${PASSPHRASE.length} chars)`); +console.log(`[BUILD] Generated build salt (${SALT.length} chars)`); +console.log(`[BUILD] Generated function name: ${FN_NAME}`); + +const scrambler = new StringScrambler(PASSPHRASE, SALT); + +// Read isSilent from the source of truth — logger.ts itself. +const loggerSource = await fs.readFile("src/utils/logger.ts", "utf-8"); +const isSilent = /const\s+isSilent\s*=\s*true/.test(loggerSource); +console.log(`[BUILD] isSilent = ${isSilent}`); + +async function transformFile(filePath: string): Promise { + const code = await fs.readFile(filePath, "utf-8"); + + console.log(`[TRANSFORM] Processing: ${filePath}`); + + // 1. Rewrite process.env.XYZ -> process.env[scramble("XYZ")] + const { code: envRewritten } = transformEnvAccess( + code, + "[TRANSFORM]", + filePath, + ); + + // 2. Scramble transform (encodes all scramble("...") calls) + const { code: scrambled } = transformSource( + envRewritten, + scrambler, + FN_NAME, + "[TRANSFORM]", + filePath, + ); + + // 3. Strip logUtil calls (only when isSilent is true) + if (isSilent) { + const { code: stripped } = stripLogCalls( + scrambled, + "[TRANSFORM]", + filePath, + ); + return stripped; + } + + return scrambled; +} + +async function walkDir(dir: string): Promise { + const files: string[] = []; + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await walkDir(fullPath))); + } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) { + files.push(fullPath); + } + } + + return files; +} + +async function build() { + console.log("[BUILD] Setting up temp directory..."); + + const tempDir = "./.bun-temp"; + await fs.rm(tempDir, { recursive: true, force: true }); + await fs.mkdir(tempDir, { recursive: true }); + + // ── Pre-build validation: no illegal scramble() usage ──── + console.log("[BUILD] Validating source files..."); + const allFiles = await walkDir("./src"); + const violations: string[] = []; + for (const file of allFiles) { + const content = await fs.readFile(file, "utf-8"); + // Pattern 1: ${scramble(...)} inside template expressions + if (/\$\{.*?scramble\(/.test(content)) { + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (/\$\{.*?scramble\(/.test(lines[i]!)) { + violations.push(` ${file}:${i + 1}: ${lines[i]!.trim()}`); + } + } + } + // Pattern 2: scramble(VARIABLE) — variable arg, not a string literal + const varArgRe = /scramble\([A-Z_][A-Z_0-9]*\)/g; + let m: RegExpExecArray | null; + while ((m = varArgRe.exec(content)) !== null) { + const lineNo = content.slice(0, m.index).split("\n").length; + violations.push(` ${file}:${lineNo}: ${m[0].trim()}`); + } + } + if (violations.length > 0) { + console.error( + `\n[BUILD] ERROR: Illegal scramble() usage detected ` + + `in ${violations.length} location(s).\n` + + `These patterns survive the build transform and cause ` + + `runtime ReferenceErrors.\n\n` + + ` ❌ \${scramble("...")} — hoist to a variable\n` + + ` ❌ scramble(VAR) — vars already contain scrambled values\n\n` + + `Violations:\n` + + violations.join("\n"), + ); + process.exit(1); + } + console.log("[BUILD] ✓ No illegal scramble() usage found"); + + console.log("[BUILD] Copying and transforming source files..."); + + const files = await walkDir("./src"); + console.log(`[BUILD] Processing ${files.length} TypeScript files`); + + for (const file of files) { + const transformed = await transformFile(file); + const tempFile = path.join(tempDir, path.relative("./src", file)); + await fs.mkdir(path.dirname(tempFile), { recursive: true }); + await fs.writeFile(tempFile, transformed, "utf-8"); + } + + const tempDecoderPath = path.join( + tempDir, + path.relative("./src", path.resolve(RUNTIME_DECODER_PATH)), + ); + const rewrittenDecoder = await rewriteRuntimeDecoder( + RUNTIME_DECODER_PATH, + PASSPHRASE, + SALT, + FN_NAME, + ); + await fs.mkdir(path.dirname(tempDecoderPath), { recursive: true }); + await fs.writeFile(tempDecoderPath, rewrittenDecoder, "utf-8"); + console.log( + `[BUILD] Injected build passphrase into ${path.relative(tempDir, tempDecoderPath)}`, + ); + + const indexPath = path.join(tempDir, "index.ts"); + + const indexCode = await fs.readFile(indexPath, "utf-8"); + await fs.writeFile( + indexPath, + `import "./utils/runtimeDecoder";\n${indexCode}`, + "utf-8", + ); + + console.log("[BUILD] Running Bun build on transformed sources..."); + + await Bun.build({ + entrypoints: [indexPath], + outdir: "./dist", + naming: { + entry: "bundle.js", + }, + target: "bun", + minify: true, + }); + + console.log("[BUILD] Cleaning up temp directory..."); + await fs.rm(tempDir, { recursive: true, force: true }); + + console.log("[BUILD] ✓ Build complete!"); +} + +build().catch(console.error);