npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dacely/toildefender

v0.2.1

Published

Modern JavaScript code protection, bytecode virtualization, and obfuscation for the Toil tech stack.

Downloads

1,734

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.

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.