diff --git a/.github/workflows/minify.yml b/.github/workflows/minify.yml
index e316084..580d29c 100644
--- a/.github/workflows/minify.yml
+++ b/.github/workflows/minify.yml
@@ -1,4 +1,4 @@
-name: Update minified CloakJS
+name: Tests and minified build
on:
push:
@@ -6,6 +6,13 @@ on:
- '**'
paths:
- cloakjs.js
+ - test/cloakjs.test.js
+ - .github/workflows/minify.yml
+ pull_request:
+ paths:
+ - cloakjs.js
+ - test/cloakjs.test.js
+ workflow_dispatch:
permissions:
contents: write
@@ -18,20 +25,87 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '22'
- - name: Minify and verify
+
+ - name: Run the test suite
+ run: node --test
+
+ - name: Minify
run: |
- node --test
npm exec --yes --package=terser@5.44.0 -- terser cloakjs.js --compress --mangle --comments '/^!/' --output cloakjs.min.js
node --check cloakjs.min.js
- node -e 'const c = require("./cloakjs.min.js"); const source = require("./cloakjs.js"); if (c.VERSION !== source.VERSION || typeof c.obfuscate !== "function") process.exit(1); const vm = require("node:vm"); const logs = []; vm.runInNewContext(c.obfuscate("console.log(\"ok\")", "high"), { console: { log: x => logs.push(x) }, atob, TextDecoder }); if (logs[0] !== "ok") process.exit(1)'
- - name: Commit minified file if changed
+
+ - name: Verify the minified build
+ run: |
+ cat > "$RUNNER_TEMP/verify-cloakjs.js" <<'VERIFY'
+ const assert = require("node:assert/strict");
+ const vm = require("node:vm");
+ const path = require("node:path");
+ const source = require(path.join(process.cwd(), "cloakjs.js"));
+ const minified = require(path.join(process.cwd(), "cloakjs.min.js"));
+
+ // metadata and API surface
+ assert.equal(minified.VERSION, source.VERSION);
+ assert.equal(minified.NAME, source.NAME);
+ assert.deepEqual(Object.keys(minified.presets), Object.keys(source.presets));
+ assert.equal(minified.techniques.length, source.techniques.length);
+ for (const key of ["obfuscate", "obfuscateWithStats", "measure", "resolveOptions"]) {
+ assert.equal(typeof minified[key], "function", `${key} is missing`);
+ }
+ assert.deepEqual(Object.keys(minified.resolveOptions("extreme")).sort(), Object.keys(source.defaultOptions).sort());
+
+ function evaluate(code, sandbox) {
+ const logs = [];
+ const scope = Object.assign({ console: { log: (value) => logs.push(String(value)) } }, sandbox);
+ vm.runInNewContext(code, scope, { timeout: 5000 });
+ return logs;
+ }
+
+ const program = [
+ 'function label(value) { var suffix = "!"; return value + suffix; }',
+ 'var list = [1, 2, 3].map(function (v) { return v * 2; });',
+ 'console.log(label("ok"), "emoji \u{1F680}", list.join("-"), 21 * 2);'
+ ].join("\n");
+ const expected = evaluate(program, { TextDecoder, atob, Uint8Array });
+
+ for (const preset of ["off", "low", "medium", "high", "extreme"]) {
+ for (let attempt = 0; attempt < 4; attempt++) {
+ const result = minified.obfuscateWithStats(program, { seed: `${preset}-${attempt}` });
+ assert.equal(result.stats.seed, `${preset}-${attempt}`);
+ assert.deepEqual(result.warnings, [], `${preset}: ${result.warnings.join("; ")}`);
+ // with the fast decoder path available
+ assert.deepEqual(evaluate(result.code, { TextDecoder, atob, Uint8Array }), expected, preset);
+ // and with no globals at all: the decoder must be self-contained
+ assert.deepEqual(evaluate(result.code, {}), expected, `${preset} (bare sandbox)`);
+ }
+ }
+
+ // the readable and the minified build must generate identical output
+ const options = {
+ seed: "determinism", stringEncryption: true, stringSplitting: true,
+ stringArrayDecoys: 4, controlFlowFlattening: true, identifierEscaping: true
+ };
+ assert.equal(minified.obfuscate(program, options), source.obfuscate(program, options));
+
+ // self-defending output runs, and rejects a patched generated function
+ const guarded = minified.obfuscate(program, { seed: 7 });
+ assert.deepEqual(evaluate(guarded, {}), expected, "guarded build produces the right output");
+ const patched = guarded.replace("String.fromCharCode", "String.fromCharCode2");
+ assert.notEqual(patched, guarded, "the decoder did not contain String.fromCharCode, the tamper check is meaningless");
+ assert.throws(() => evaluate(patched, {}), /Unexpected token/);
+
+ console.log("minified build verified:", minified.VERSION);
+ VERIFY
+ node "$RUNNER_TEMP/verify-cloakjs.js"
+
+ - name: Commit the minified file if it changed
+ if: github.event_name == 'push' && github.ref_type == 'branch'
run: |
git add -- cloakjs.min.js
if git diff --cached --quiet; then
- echo 'Minified file is already up to date.'
+ echo 'cloakjs.min.js is already up to date.'
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
- git commit -m 'chore: update minified CloakJS'
- git push origin "HEAD:${GITHUB_REF}"
+ git commit -m 'chore: rebuild cloakjs.min.js'
+ git push origin "HEAD:${{ github.ref_name }}"
diff --git a/README.md b/README.md
index ebe4f53..fbb334a 100644
--- a/README.md
+++ b/README.md
@@ -1,101 +1,213 @@
-# CloakJS 1.2.0
+# CloakJS 1.3.0
[](LICENSE)
[](https://github.com/Infinitode/CloakJS/stargazers)
[](https://github.com/Infinitode/CloakJS/issues)
[](https://www.jsdelivr.com/package/gh/Infinitode/CloakJS)
-CloakJS is a single-file, dependency-free JavaScript obfuscator for the browser and Node.js. It can rename some identifiers, encode strings and numbers, and optionally add a dispatcher and decoy statements. It returns JavaScript source code; it does **not** run the code for you.
+CloakJS is a single-file, dependency-free JavaScript obfuscator for the browser and Node.js. It renames bindings, hides strings behind a generated decoder, rewrites numeric literals, flattens control flow inside function bodies, injects opaque predicates and unreachable decoy code, escapes identifiers, and can add runtime guards. It returns JavaScript source **as a string** — it never executes the code for you.
+
+```html
+
+
+```
> [!IMPORTANT]
-> Obfuscation makes source less readable, **not secure**. Never put secrets, credentials or security-critical checks in client-side JavaScript. CloakJS uses a small lexer rather than a full JavaScript parser; test transformed output in the environment where it will run. It is intended for classic scripts, not ES modules, JSX or TypeScript.
+> **CloakJS is not distributed through npm.** There *is* a package named `cloakjs` on the npm registry, but it is an unrelated AOP/Proxy wrapper library ("a small AOP/Proxy/Wrapper library with a fluent interface", currently 1.0.1, maintained by somebody else) — it is not this project and it does not contain this obfuscator. Do not install that package expecting CloakJS. Grab [`cloakjs.js`](cloakjs.js) or [`cloakjs.min.js`](cloakjs.min.js) from this repository (or from jsDelivr) instead; the library has no dependencies and no package manifest, so there is nothing to resolve and nothing to build.
+
+> [!WARNING]
+> Obfuscation makes source **harder to read, not secure**. It cannot protect secrets, credentials, licence keys or security-critical checks that ship to the client, and it does not resist an analyst who can instrument the running page. Everything CloakJS does is reversible by hand or by tooling. Treat it as a speed bump and a deterrent — never as a security boundary.
+
+## What changed in 1.3.0
+
+- **`extreme` is a real mode now.** In 1.2.0 its options were identical to `high`; it now turns on identifier escaping, codepoint-safe string splitting, decoy string-array entries, maximum flattening and decoy amounts, and a recurring `debugger` trap.
+- **Control-flow flattening reaches inside function bodies** (nested bodies are flattened before the bodies that contain them), with a shuffled `case` layout and decoy cases that can never be selected.
+- **A self-contained decoder.** Generated code no longer depends on `atob`, `Uint8Array` or `TextDecoder`: it ships its own Base64, alphabet-substitution, RC4 and UTF-8 handling and only uses `TextDecoder` as a fast path when the host has one.
+- **Template literals are first class.** Identifiers inside `` `${…}` `` are analysed and renamed, and static templates are encrypted like ordinary strings (tagged templates are left alone).
+- **Much broader, scope-aware mangling.** Parameters (including defaults and rest), `catch` bindings, class names, named function expressions and template substitutions are renamed — but a spelling is only renamed when *every* occurrence lies inside a scope that declares it, so globals owned by other scripts keep working.
+- **New options:** `stringSplitting` (+ `MinLength`, `MaxParts`), `stringArrayShuffle`, `stringArrayDecoys`, `stringEncoding`, `stringAlphabet`, `identifierEscaping`, `preserveFunctionNames`, `controlFlowThreshold`, `deadCodeAmount`, `debugProtection` (+ `Interval`), `wrapInIife`, `disableConsoleOutput`, `compactOutput`, `seed`.
+- **Validated options with types.** A typo like `stringEncrypton` now throws a `TypeError` instead of being silently ignored, and wrong value types are rejected before anything runs.
+- **Reproducible output** via `seed`, richer `stats` (including the `techniques` that were actually applied) and clearer `warnings`.
## Getting started
-Download [`cloakjs.js`](cloakjs.js) (readable) or [`cloakjs.min.js`](cloakjs.min.js) (minified), or load either from [jsDelivr](https://www.jsdelivr.com/package/gh/Infinitode/CloakJS) after the files are available on the repository's `main` branch:
+Download [`cloakjs.js`](cloakjs.js) (readable) or [`cloakjs.min.js`](cloakjs.min.js) (minified), or load either from [jsDelivr](https://www.jsdelivr.com/package/gh/Infinitode/CloakJS):
```html
-
+
-
-
+
```
-For reproducible deployments, replace `@main` with a published tag or commit hash (once available). The minified file is generated from `cloakjs.js` with Terser; there is no npm release to install. The badge above displays jsDelivr's live download information rather than a hard-coded count and may not show data until the files have been published on `main`.
+For reproducible deployments, replace `@main` with a tag or commit hash from this repository. The minified file is generated from `cloakjs.js` with Terser; there is no npm release. The badge above shows jsDelivr's live download information rather than a hard-coded count.
-In Node.js (modern Node with `atob`, `TextEncoder` and `TextDecoder`):
+In Node.js (any modern Node, no extra packages):
```js
-const CloakJS = require('./cloakjs.js');
-const result = CloakJS.obfuscateWithStats('console.log("hello")', 'high');
-console.log(result.code);
-console.log(result.stats, result.warnings);
+const CloakJS = require("./cloakjs.js");
+
+const result = CloakJS.obfuscateWithStats('const who = "world"; console.log(`hi ${who}`);', "extreme");
+console.log(result.code); // obfuscated source, as a string
+console.log(result.stats); // what the pipeline actually applied
+console.log(result.warnings); // anything it refused to do — check these
+
+// reproducible output while you are building a pipeline
+const first = CloakJS.obfuscate("var a = 1; console.log(a);", { seed: "build-42" });
```
-The file is UMD: it also works as an AMD module, or as the global `CloakJS` in a normal `