Obfuscate JavaScript inside a Cloudflare Worker

Run AfterPack in your own Cloudflare Worker with @afterpack/wasm, pass your Pro key to build on AfterPack's cloud, and cache each result per rotation window.

AfterPack runs from a Cloudflare Worker in two ways, through the same package and the same call. Without a key, the Free engine runs inside your Worker, as a WebAssembly module you install from npm. With your Pro key, the build runs on AfterPack's cloud with the Pro engine.

This page covers both, then how to cache the result so each asset is built once per rotation window.

AfterPack, inside your own Worker

@afterpack/wasm is AfterPack compiled to WebAssembly. It is the same Rust engine the CLI and the framework plugins run, built for wasm32 instead of your machine's architecture.

npm install @afterpack/wasm
import { obfuscate } from "@afterpack/wasm";

export default {
  async fetch(request) {
    const source = await request.text();
    const result = await obfuscate({ path: "app.js", source }, { preset: "hard" });
    return new Response(result.bytes, {
      headers: { "content-type": "application/javascript" },
    });
  },
};

That is the whole integration. No init call, no nodejs_compat flag, no wrangler rule for the .wasm file. The module has no WebAssembly imports, so it instantiates synchronously the first time you call obfuscate, and the package's exports map hands your bundler a Worker-shaped entry on its own. The same import works in Node, where a different entry reads the binary off disk.

The surface is identical to @afterpack/core — obfuscate, obfuscateAll, version, all async, all throwing on a build that cannot ship. Configuration is the same object documented on Configuration, including preset and seed.

Use `@afterpack/core` unless you can't

@afterpack/core ships a native binary and is faster. Reach for @afterpack/wasm when the host cannot load one: a Worker, a sandboxed CI image, an edge function.

What it costs you

The engine binary ships inside the package, already compiled.

BytesSize
Raw .wasm4,573,3214.36 MiB
Gzipped1,307,7721.25 MiB

Cloudflare's Worker size limit is 3 MB compressed on the free plan and 10 MB on paid, so the engine fits either. What the free plan does not fit is the work: it caps CPU at 10 ms per invocation, and obfuscating a 185 KB bundle on a laptop core takes under 0.3 seconds at the default light preset and about 1 second at medium. Anything past a demo wants Workers Paid.

A Worker that obfuscates the same asset on every request pays that CPU every time. Caching the result for a rotation window, or doing the work in your build and serving the output, keeps the bill down where a new shape per request is not what you are after.

What the package you host can and cannot do

The .wasm you install performs Free protection. Pro transforms run only on AfterPack's infrastructure, so the package holds nothing to unlock: no key check to bypass, no switch that turns a hosted copy into a Pro build. A key routes work to the cloud; it does not enable anything locally.

The published package carries an npm provenance record — package version, the workflow that built it, and the artifact digest — so you can tie the bytes you installed to the build that produced them.

Pro, from your own Worker

Pass your key in the config of the same obfuscate() call, for example from a Worker secret, and the build runs on AfterPack's cloud with the Pro engine:

import { obfuscate } from "@afterpack/wasm";

export default {
  async fetch(request, env) {
    const source = await request.text();
    const result = await obfuscate(
      { path: "app.js", source },
      { preset: "hard", key: env.AFTERPACK_KEY },
    );
    return new Response(result.bytes, {
      headers: { "content-type": "application/javascript" },
    });
  },
};

The package makes the request to POST /v1/obfuscate/batch for you, so a Worker never codes the API by hand. Files too big for a Worker move to containers automatically. The Pro engine never ships into your Worker: it runs only on AfterPack's infrastructure, which is the tier boundary above.

Each call is a metered Pro build (what Pro costs) plus a fast request inside Cloudflare's network. You can build on every request, but each one is billed, so cache the result for a rotation window instead.

Your source leaves your Worker and is processed in AfterPack's cloud, in memory and not persisted, per Privacy & data handling. The call fails closed if the cloud cannot be reached: it throws and nothing unprotected ships. The Cloud API reference is the full wire contract, and Deployment modes explains what changes when the engine moves.

Pro will never ship as a wasm you host

A package that claims to run Pro transforms on your own infrastructure is not ours. Check the npm provenance record before installing anything under this name.

Cache the result for a rotation window

Derive the seed from the current time window and cache the result under a key that includes the window. Each location then builds an asset once per window and serves it until the window ends, on Free and on Pro alike:

import { obfuscate } from "@afterpack/wasm";
import source from "./pricing.client.js";

const WINDOW_SECONDS = 600;

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname !== "/pricing.js") {
      return new Response("Not found", { status: 404 });
    }

    const now = Math.floor(Date.now() / 1000);
    const slot = Math.floor(now / WINDOW_SECONDS);
    const secondsLeft = (slot + 1) * WINDOW_SECONDS - now;
    const cacheKey = new Request(`${url.origin}/pricing.js?slot=${slot}`);

    let response = await caches.default.match(cacheKey);
    if (!response) {
      const result = await obfuscate(
        { path: "pricing.js", source },
        { preset: "hard", seed: `pricing.js:${slot}`, key: env.AFTERPACK_KEY },
      );
      response = new Response(result.bytes, {
        headers: {
          "content-type": "application/javascript",
          "cache-control": `public, max-age=${secondsLeft}`,
        },
      });
      ctx.waitUntil(caches.default.put(cacheKey, response.clone()));
    }

    response = new Response(response.body, response);
    response.headers.set("cache-control", `public, max-age=${secondsLeft}`);
    return response;
  },
};

Without an AFTERPACK_KEY secret, key is undefined and the same Worker builds on Free inside your Worker. The source file is imported as a string through a wrangler Text rule, as in the full walkthrough.

The window is in both the seed and the cache key, so every location running the same deployment serves the same bytes for a window, and a new window never reads the old entry. The outgoing cache-control counts down to the end of the window, so a browser never keeps a copy past it. The Cache API stores entries per data center and has no effect in the dashboard editor, so test it with wrangler dev or a deployed route.

Choosing

You wantUse
Obfuscation in a Worker, nothing leaves your account@afterpack/wasm, this page
Obfuscation in a normal Node build@afterpack/core or the CLI
Pro hardening and per-region directives, from a WorkerThe same call with a key, which builds on AfterPack's cloud
A new shape per window without a build per visitorThe rotation-window cache

Next