Skip to content

dacely-cloud/toildefender

Repository files navigation

ToilDefender

TypeScript-first JavaScript code protection for the Toil stack.

Typed ESM/CommonJS package with randomized control flow, literal protection, object packing, BigInt-backed VM bytecode, and hash-mesh bytecode unlock for browser and Node bundles.


npm node vm license


ToilDefender is Dacely's maintained TypeScript-authored JavaScript protection layer for the Toil technology stack. It started from the original defendjs project, but this fork is now maintained as its own typed package: @dacely/toildefender.

The goal is not to make client-side JavaScript impossible to analyze. That is not a real promise. The goal is to raise reverse-engineering cost by removing source-level structure, splitting logic across generated helpers, packing constants, and optionally compiling selected functions into randomized numeric VM programs.

npm install @dacely/toildefender
import { protect, type ToilDefenderOptions } from "@dacely/toildefender";

const source = `
function licenseGate(input) {
    return input.length > 8 && input.charCodeAt(0) === 84;
}

globalThis.__result = licenseGate("ToilDefender");
`;

const options: ToilDefenderOptions = {
    code: source,
    modulesCode: {},
    logLevel: "error",
    features: {
        dead_code: true,
        scope: true,
        control_flow: true,
        identifiers: true,
        numeric_vm: true,
        object_packing: true,
        literals: true,
        mangle: true,
        compress: true
    },
    protections: {
        virtualMachine: {
            enabled: true,
            mode: "aggressive",
            bigintBytecode: true,
            randomizedOpcodes: true,
            encodeConstants: true,
            perFunctionDialect: true,
            virtualize: "heuristic"
        },
        hashMesh: {
            enabled: true,
            mode: "aggressive",
            unlock: "per-function",
            deriveDialectFromMesh: true,
            bindToVmState: true,
            encodeChaff: true,
            chaffRatio: 0.55
        }
    }
};

const result = protect(options);

console.log(result.code);

Package Format

The public package is authored in TypeScript and built with Vite 8. Published artifacts are emitted into build/ as dual module targets:

Consumer Entry
ESM import toildefender, { protect, do, features } from "@dacely/toildefender"
CommonJS Legacy compatibility through the package exports.require entry.
Types import type { ToilDefenderOptions, ToilDefenderResult } from "@dacely/toildefender"
CLI toildefender --help

protect(options) and do(options) are the same public function. The default export keeps the classic object shape: { protect, do, features }.

The package requires Node >=24.11.0. Type declarations are generated from the source and shipped through the package exports map, so TypeScript consumers do not need @types packages.

What It Does

Protection Purpose
control_flow Rewrites structured control flow into dispatcher-style execution.
scope Flattens function/scope structure into generated runtime frames.
identifiers Renames and rewrites identifiers, object references, and property access.
object_packing Packs object literal keys into numeric schemas instead of readable key/value arrays.
literals Encodes strings and numeric constants.
dead_code Inserts unreachable or low-value code paths to add noise.
mangle Shortens generated identifiers.
compress Emits compact output.
numeric_vm Virtualizes supported functions into BigInt-packed bytecode.

Virtual Machine Protection

Transform your JavaScript into randomized virtual-machine bytecode for maximum resistance against reverse engineering.

ToilDefender compiles protected functions into a private instruction set, packs the bytecode into encrypted BigInt streams, and executes it through a generated runtime VM. Instead of exposing readable JavaScript logic, your code becomes numeric program data consumed by a randomized virtual machine.

The compiler also fuses selected hot stack patterns into semantic superinstructions, so common operation boundaries such as constant-key property reads are not always emitted as separate primitive VM opcodes. Constants are wrapped in access-bound cells, so encoded strings and references are decoded lazily when bytecode reads them instead of during VM call setup.

Original logic disappears from the output bundle. Attackers no longer reverse plain JavaScript; they must recover the VM, decode the bytecode format, reconstruct the instruction set, and emulate the protected program.

import { protect, type ToilDefenderOptions } from "@dacely/toildefender";

const source = `
function validateLicense(input) {
    const total = input.length * 7;
    return input.charCodeAt(0) === 86 ? total + 13 : total - 5;
}

globalThis.__result = validateLicense("VM-Protected");
`;

const options: ToilDefenderOptions = {
    code: source,
    modulesCode: {},
    features: {
        numeric_vm: true
    },
    protections: {
        virtualMachine: {
            enabled: true,
            mode: "aggressive",
            bigintBytecode: true,
            randomizedOpcodes: true,
            encodeConstants: true,
            perFunctionDialect: true,
            virtualize: "marked",
            minFunctionSize: 1,
            maxFunctionSize: 120,
            seed: "build-seed"
        }
    }
};

const result = protect(options);

Selection modes:

virtualize Meaning
marked Virtualize functions marked by supported annotations or compiler selection.
all-supported Virtualize every function that fits the supported syntax subset.
heuristic Virtualize functions selected by size and compiler suitability.

Supported VM syntax currently targets practical protection work: literals, locals, arguments, return, assignment, arithmetic, comparisons, logical expressions, if / else, while, calls, member reads, arrays, and object literals. Unsupported syntax remains native or is skipped by selection.

All-Modes Output Demo

Input:

const demoSource = `
function licenseGate(input) {
    const total = input.length * 7;
    return input.charCodeAt(0) === 86
        ? { ok: true, total: total + 13 }
        : { ok: false, total: total - 5 };
}

globalThis.__result = licenseGate("ToilDefender");
`;

The demo artifact is generated with every major protection enabled and compression disabled so the runtime stays readable:

import { protect, type ToilDefenderOptions } from "@dacely/toildefender";

const demoOptions: ToilDefenderOptions = {
    code: demoSource,
    modulesCode: {},
    features: {
        dead_code: true,
        scope: true,
        control_flow: true,
        identifiers: true,
        numeric_vm: true,
        object_packing: true,
        literals: true,
        mangle: true,
        compress: false
    },
    protections: {
        virtualMachine: {
            enabled: true,
            mode: "aggressive",
            bigintBytecode: true,
            randomizedOpcodes: true,
            encodeConstants: true,
            perFunctionDialect: true,
            virtualize: "all-supported",
            seed: "readme-all-modes-demo"
        },
        hashMesh: {
            enabled: true,
            mode: "aggressive",
            unlock: "per-function",
            deriveDialectFromMesh: true,
            bindToVmState: true,
            encodeChaff: true,
            chaffRatio: 0.55
        }
    }
};

const demoResult = protect(demoOptions);

The complete beautified generated output is committed at docs/all-modes-output.demo.js. It is a real 1019-line artifact from the current generator and executes to:

Output excerpt:

(function () {
  function a(f, k) {
    var b = new Array(109);
    ;
    var c = arguments;
    var i;
    while (true) try {
      switch (f) {
        case 24210:
          b[11] = c[11];
          b[12] = c[10];
          b[13] = c[9];
          b[14] = c[8];
          b[15] = c[7];
          b[16] = c[6];
          b[17] = c[5];
          b[18] = c[4];
          b[19] = c[3];
          b[20] = c[2];
          b[21] = e(a, 16503, b, c[1]);
          b[22] = e(a, 16827, b, c[1]);
          b[23] = e(a, 28881, b, c[1]);
          b[24] = e(a, 27718, b, c[1]);
          b[25] = e(a, 26046, b, c[1]);
          b[26] = e(a, 11984, b, c[1]);
          b[27] = e(a, 10989, b, c[1]);
          b[28] = e(a, 10700, b, c[1]);
          b[29] = e(a, 18606, b, c[1]);
          b[30] = e(a, 22347, b, c[1]);
          b[31] = e(a, 28683, b, c[1]);
          b[32] = e(a, 11069, b, c[1]);
          b[33] = e(a, 8443, b, c[1]);
          b[34] = e(a, 27840, b, c[1]);
          b[35] = e(a, 21656, b, c[1]);
          b[36] = BigInt(b[19]);
          b[37] = [1n];
          b[38] = c[1][10][1];
          b[39] = c[1][10][1];
          if (b[11]) {
            b[38] = c[1][4](b[11], b[19], b[18], b[17], b[16], b[12]);
            b[39] = b[11][c[1][10][24]] >>> c[1][10][1];
          }
          b[40] = c[1][10][1];
          b[41] = b[17] >>> c[1][10][1];
          while (b[40] < b[18]) {
            b[42] = b[33](b[40]);
            b[41] = b[34](b[41], b[42], b[40]);
            b[40] += c[1][10][5];
          }
          if (b[41] >>> c[1][10][1] !== b[16] >>> c[1][10][1]) throw new Error(c[1][10][29]);
          b[43] = c[1][10][1];
          b[44] = b[17] >>> c[1][10][1];
          b[45] = b[17] & c[1][10][5];
          b[46] = b[45] ? c[1][10][30] : [];
          b[47] = b[45] ? c[1][10][30] : [];
          b[48] = b[45] ? g([
            /* encoded layout keys */
          ], [
            [],
            Object[c[1][10][36]](c[1][10][30])
          ]) : c[1][10][30];

          /* 900+ more generated lines:
             dispatcher cases, encoded literals, streaming VM token reads,
             lazy constant cells, seed-selected stack/local storage, BigInt program blobs,
             semantic superinstructions, randomized opcode tables,
             and Hash-Mesh unwrap */

        case 27718:
          if (c[1][50] < c[2][10][1] || c[1][50] >= c[1][18]) throw new Error(c[2][10][46]);
          b[1] = c[1][31](c[1][50]);
          c[1][50] += c[2][10][5];
          return b[1];
        case 30063:
          b[1] = '';
          b[1] += d(86, 101, 105);
          b[1] += d(108, 109);
          b[1] += d(97, 114, 107);
          return b[1];
      }
    } catch (a) {
      i = null;
      switch (f) {
        default:
          throw a;
      }
    }
  }
  a(20498, {});
})();
{ "ok": true, "total": 69 }

That output contains the full stacked mess: flattened dispatcher runtime, identifier rewriting, packed literals, object packing, VM bytecode execution, BigInt program blobs, randomized opcode tables, and Hash-Mesh unlock material.

Hash-Mesh Unlock

Hash-Mesh Unlock derives VM bytecode keys from runtime integrity data. If protected code, constants, VM helpers, or execution state are modified, the next bytecode chunk decrypts incorrectly instead of exposing runnable logic.

This turns integrity checks into decryption requirements instead of patchable boolean branches.

import { protect, type ToilDefenderOptions } from "@dacely/toildefender";

const source = `
function licenseGate(input) {
    const total = input.length * 7;
    return input.charCodeAt(0) === 86 ? total + 13 : total - 5;
}

globalThis.__result = licenseGate("Hash-Mesh");
`;

const options: ToilDefenderOptions = {
    code: source,
    modulesCode: {},
    features: {
        numeric_vm: true
    },
    protections: {
        virtualMachine: {
            enabled: true,
            mode: "aggressive",
            virtualize: "all-supported"
        },
        hashMesh: {
            enabled: true,
            mode: "aggressive",
            unlock: "per-function",
            deriveDialectFromMesh: true,
            bindToVmState: true,
            encodeChaff: true,
            chaffRatio: 0.55,
            serverBound: false
        }
    }
};

const result = protect(options);

Hash-Mesh is an obfuscation and tamper-resistance layer. It is not a cryptographic secrecy guarantee for code running on an attacker-controlled machine.

CLI

Install globally or run through npx:

npm install -g @dacely/toildefender
toildefender --help
toildefender \
  --input ./src \
  --output ./dist-protected \
  --features scope,control_flow,identifiers,literals,mangle,compress

For multi-entry projects, declare entry files in package.json:

{
    "toildefender": {
        "mainFiles": ["index.js", "worker.js"]
    }
}

The old defendjs.mainFiles field is still read as a compatibility fallback, but new projects should use toildefender.

API And Types

ESM and TypeScript:

import toildefender, {
    protect,
    type FeatureConfig,
    type ToilDefenderOptions,
    type ToilDefenderResult
} from "@dacely/toildefender";

const features: Partial<FeatureConfig> = {
    dead_code: false,
    scope: true,
    control_flow: true,
    identifiers: true,
    numeric_vm: false,
    object_packing: true,
    literals: true,
    mangle: true,
    compress: true
};

const options: ToilDefenderOptions = {
    code: "function add(a,b){ return a + b } globalThis.x = add(1,2)",
    modulesCode: {},
    logLevel: "warn",
    features
};

const result: ToilDefenderResult = protect(options);

console.log(result.code);
console.log(toildefender.features.control_flow.default);

Named exports are available in ESM as protect, do, and features. The default export keeps the compatibility object with the same members. CommonJS callers remain supported through the package exports map, but new code should use the typed ESM API.

Exported TypeScript types:

Type Purpose
ToilDefenderOptions Full input configuration for protect / do.
ToilDefenderResult Protected output object with code and optional map.
FeatureName, FeatureConfig, FeatureDescriptions Feature switch and metadata types.
ProtectionOptions, NumericVmOptions, HashMeshOptions VM and Hash-Mesh configuration.
ControlFlowOptions, ScopeOptions Seed and ratio controls for those passes.
LogLevel, LogAdapter Typed logging integration.

Main options:

Option Meaning
code Entry source code.
modulesCode Map of dependency filename to source code.
features Feature switches for the classic pipeline.
forceFeatures Compatibility/testing override for feature selection after option merging; normal callers should use features.
babel Defaults to false; set to true only when you want the optional Babel downlevel transform before protection.
babelPreserveAsync Defaults to true; when babel: true, keeps async/generator syntax native so async-aware flattening can avoid Babel regenerator helper bloat. Set to false for legacy async lowering.
babelTarget Babel target string used only when optional Babel lowering is enabled.
protections.virtualMachine User-facing VM bytecode backend configuration.
protections.hashMesh User-facing hash-mesh unlock configuration.
numericVm Lower-level numeric VM configuration retained for internal callers.
controlFlow Control-flow pass seed and ratio.
scope Scope-flattening pass seed and ratio.
preprocessorVariables Compile-time preprocessor constants.
runtimeHelpers Controls whether generated runtime helpers are emitted.
simplify Enables the post-generation simplify pass.
logLevel error, warn, info, debug, or log.
logAdapter Receives typed log callbacks when custom logging is needed.

The default path parses modern syntax directly and normalizes the constructs that older obfuscation passes cannot consume yet. The native AST path supports plain classes as native islands, class fields, private fields, arrows, for-of loops, async/generator functions, optional chaining, nullish coalescing, object rest/spread, and spread calls.

When babel: true and babelPreserveAsync is enabled, optional Babel packages installed by the caller can still downlevel syntax for legacy browser targets while leaving async and generator functions for ToilDefender's async/generator dispatchers. This avoids the large regenerator helper path for modern browser and Node bundles.

Toil Integration

ToilDefender is intended to sit behind Toil build tooling. Framework packages can call the API directly, then run normal syntax validation and browser tests against the protected artifact.

Recommended Toil stack pattern:

source bundle
-> Vite / framework build
-> ToilDefender pre-obfuscation and protection
-> syntax validation
-> browser smoke tests
-> publish/deploy artifact

For security-sensitive browser code, pair this with server-side validation. Client-side protection raises cost; it does not replace server authority.

Security Boundary

ToilDefender is code protection, not magic.

It helps against:

  • quick static reading of shipped JavaScript
  • simple string/signature extraction
  • source-level control-flow recovery
  • direct patching of obvious boolean integrity checks
  • automated diffing across builds when seeds and dialects rotate

It does not guarantee:

  • secrets stay secret in client-side code
  • runtime tracing is impossible
  • browser-controlled attackers cannot eventually emulate behavior
  • server authorization can be moved into the browser

Put real authorization and durable decisions on the server.

Development

npm run build
npm run typecheck
npm run lint
npm test
npm run test:firefox
npm run pack:dry

Source lives under src/ as TypeScript. npm run build runs the Vite 8 library build for ESM and CommonJS, then emits declaration files with tsconfig.build.json.

Generated package artifacts live under build/ and are intentionally ignored in git. The root toildefender.js and defendjs.js shims load the built CLI and library output for package compatibility.

The regression suite covers modern syntax handling, object packing, VM bytecode execution, Hash-Mesh unlock, and tamper failure behavior.

Credit

ToilDefender began as a fork of defendjs, originally created by Alexander Horn and released under the GNU Affero General Public License v3.0.

Dacely maintains this fork for the Toil stack and has added the modern parser surface, VM bytecode backend, Hash-Mesh unlock layer, object key packing, branding cleanup, and current regression coverage.

See NOTICE.md for attribution details.

License

AGPL-3.0. See LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors