@dacely/toildefender
v0.2.1
Published
Modern JavaScript code protection, bytecode virtualization, and obfuscation for the Toil tech stack.
Downloads
1,734
Maintainers
Readme
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.
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/toildefenderimport { 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 --helptoildefender \
--input ./src \
--output ./dist-protected \
--features scope,control_flow,identifiers,literals,mangle,compressFor 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 artifactFor 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:drySource 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.
