Upload files to "scripts"
This commit is contained in:
@@ -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<string, unknown>\)\.beautify\s*=\s*\(s:\s*string\):\s*string\s*=>\s*s;?\s*\n?/m;
|
||||
const SCRAMBLE_IDENTITY = /^\(globalThis as Record<string, unknown>\)\.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<string> {
|
||||
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<string[]> {
|
||||
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);
|
||||
@@ -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",
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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<string> {
|
||||
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<string[]> {
|
||||
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);
|
||||
Reference in New Issue
Block a user