# Обфускація JavaScript на кожен запит у Cloudflare Workers

Щоб обфускувати JavaScript на кожен запит у Cloudflare Worker, викличте @afterpack/wasm від AfterPack з випадковим seed або кешуйте одну версію на вікно часу.

Source: https://www.afterpack.dev/uk/blog/obfuscate-javascript-cloudflare-workers

Published: 2026-09-28 · Author: Nikita Savchenko · Tags: guide, security

Щоб обфускувати JavaScript на кожен запит у Cloudflare Worker, встановіть `@afterpack/wasm` і викличте `obfuscate()` у своєму обробнику `fetch` з випадковим `seed`: кожна відповідь буде структурно іншим скриптом, що поводиться так само. Пакет є рушієм AfterPack (afterpack.dev), обфускатора JavaScript, скомпільованим у WebAssembly; він безкоштовний і працює всередині вашого Worker. Реальні бандли перевищують ліміт у 10 мс CPU безкоштовного плану, тож беріть Workers Paid і кешуйте одну обфусковану версію на вікно часу.

Усе нижче я запускав через `wrangler dev`: точний Worker, вивід curl із двома різними тілами для однієї URL і варіант із кешем, що змінюється кожні десять хвилин, а не на кожен запит. Обфускація коду на кожен запит вміщується в один npm-пакет і менше ніж двадцять рядків коду Worker.

ШІ здешевив читання JavaScript, який ви відвантажуєте: ШІ-агенту [вистачило кількох хвилин](https://www.afterpack.dev/blog/ai-deobfuscates-javascript), щоб перетворити обфусковані демо назад на чистий вихідний код. [CLI AfterPack](https://www.afterpack.dev/docs/cli) і плагіни для фреймворків відповідають на це на рівні релізу: новий seed на кожну збірку ([пост про запуск](https://www.afterpack.dev/blog/announcing-afterpack-javascript-obfuscator)). У вашому власному Worker той самий рушій переносить цей ритм на кожен запит, тож будь-яку автоматизацію, написану під одну копію скрипта, доводиться переробляти під наступну.

## Що зупиняє обфускація коду на кожен запит?

Хуки, патчі та скрипти деобфускації, зібрані під одну відповідь, перестають працювати на наступній, бо ідентифікатори, зсуви й позиції декодера щоразу рухаються. Це важливо для скриптів, до яких автоматизація повертається знову і знову: антибот- і challenge-скриптів, клієнтських перевірок цілісності та скриптів, які скрапери патчать або перехоплюють, щоб їх обійти.

Кожна відповідь до того ж є повною збіркою AfterPack: алгоритми переписано, потік керування сплющено, ідентичності злито, тож навіть відновлена копія відрізняється від коду, який ви написали. Проте всі відповіді залишаються однією програмою, і [жоден обфускатор, зокрема AfterPack, не робить відновлення неможливим](https://www.afterpack.dev/docs/threat-model#what-obfuscation-protects).

## Як встановити обфускатор JavaScript у Cloudflare Worker

Встановіть `@afterpack/wasm` поруч із `wrangler` та імпортуйте з нього `obfuscate`. Немає виклику `init`, прапорця `nodejs_compat` чи правила wrangler для файлу `.wasm`.

Версії, які я використовував 2026-09-27: `@afterpack/wasm` 0.1.0, `wrangler` 4.141.0, Node 24.21.0, на Apple M2 Max.

```bash
npm i @afterpack/wasm wrangler
```

`@afterpack/wasm` — це той самий рушій на Rust, що працює в CLI і плагінах, скомпільований у WebAssembly; мапа `exports` пакета дає wrangler точку входу у формі Worker ([документація](https://www.afterpack.dev/docs/workers)). Той самий імпорт працює в Node, де інша точка входу читає бінарник із диска, хоча у звичайній збірці на Node нативний [`@afterpack/core`](https://www.afterpack.dev/docs/frameworks) швидший.

Скрипт у демо — невеликий клієнтський калькулятор цін. Він заміняє challenge-скрипт або скрипт перевірки цілісності і достатньо короткий, щоб звірити вивід вручну:

**src/pricing.client.js — вихідний код, який віддається**

```js
const TIERS = [
  { min: 1, max: 9, unit: 12 },
  { min: 10, max: 49, unit: 10 },
  { min: 50, max: Infinity, unit: 8 },
];

function unitPrice(quantity) {
  const tier = TIERS.find((t) => quantity >= t.min && quantity <= t.max);
  return tier ? tier.unit : TIERS[0].unit;
}

export function quote(quantity, coupon) {
  let total = unitPrice(quantity) * quantity;
  if (coupon === "LAUNCH20" && quantity >= 10) total *= 0.8;
  return Math.round(total * 100) / 100;
}
```

Worker імпортує цей файл як рядок, для чого потрібне одне правило wrangler для вихідного файлу (рушію воно не потрібне). `wrangler.jsonc`:

```jsonc
{
  "name": "per-request-obfuscation",
  "main": "src/worker.js",
  "compatibility_date": "2026-09-25",
  "vars": { "WINDOW_SECONDS": "600" },
  "rules": [{ "type": "Text", "globs": ["**/*.client.js"], "fallthrough": true }]
}
```

## Як обфускувати JavaScript на кожен запит

Згенеруйте випадковий seed в обробнику `fetch`, передайте його в `obfuscate()` і поверніть результат із `cache-control: no-store`. Тоді кожна відповідь збирається зі свого власного seed.

`src/worker.js`:

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

export default {
  async fetch(request) {
    if (new URL(request.url).pathname !== "/pricing.js") {
      return new Response("Not found", { status: 404 });
    }
    const seed = crypto.getRandomValues(new Uint32Array(1))[0];
    const result = await obfuscate({ path: "pricing.js", source }, { preset: "hard", seed });
    return new Response(result.bytes, {
      headers: {
        "content-type": "application/javascript",
        "cache-control": "no-store",
      },
    });
  },
};
```

У Worker завжди передавайте [`seed`](https://www.afterpack.dev/docs/config#seed) самі: саме випадковий seed із `crypto.getRandomValues` робить кожну відповідь іншою. `cache-control: no-store` це зберігає, бо браузер, інший CDN перед вашим доменом або корпоративний проксі інакше могли б залишити собі першу версію, і робота на кожен запит нічого б не дала. Щоб відтворити відповідь пізніше, логуйте її seed або повертайте його в заголовку відповіді; seed не є секретом.

Запустіть `npx wrangler dev` і двічі запитайте ту саму URL:

```bash
$ for i in 1 2; do curl -s localhost:8787/pricing.js | shasum -a 256; done
0265ae6f5cf5a0839ee52a9a08bab6326503849c66296ae581c2c21923d2d676  -
a5684abbb2ba13be3c182840d30b1f2a634b241013e112be70c6de22ac1233b4  -

$ for i in 1 2; do curl -s localhost:8787/pricing.js | head -c 100; echo; done
var DA=($d,wS,Uv)=>((tD*(tD+1)&1)===0?(((tD*tD*tD-tD)%3|0)===0?$d:tD^32)[((tD*tD-tD&1)===0?e:tD&739)
var Sg=(H5,xS,nF)=>((Ye*(Ye+1)*(Ye+5)%6|0)===0?H5:Ye+124)[((Ye*(Ye+3)%2|0)!==0?Ye+100:B)](((Ye*(Ye+1

$ for i in 1 2; do curl -s localhost:8787/pricing.js | wc -c; done
    3823
    3545
```

Хеші, ідентифікатори й довжини різняться, і жодне тіло не містить `TIERS` чи `unitPrice`. Я зберіг дві відповіді як модулі й викликав `quote()` у кожній поруч з оригіналом для шести пар кількість/купон; усі три повернули ті самі суми. Рядок купона всередині лишається, закодований і декодований під час виконання, і це нормально: сервер, що приймає замовлення, перераховує ціну й перевіряє купон, а ця клієнтська копія лише показує розрахунок.

## Як кешувати обфускований JavaScript і змінювати його кожні 10 хвилин

Виводьте seed із поточного вікна часу замість випадкового і кешуйте результат за ключем, що містить вікно. Тоді кожна локація збирає одну обфусковану версію на вікно і віддає її до кінця вікна, тож відвідувачі більше не платять CPU кожен за свою збірку:

**src/rotating.js — одна версія на вікно, з кешем**

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

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 windowSeconds = Number(env.WINDOW_SECONDS);
    const now = Math.floor(Date.now() / 1000);
    const slot = Math.floor(now / windowSeconds);
    const secondsLeft = (slot + 1) * windowSeconds - 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}` },
      );
      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;
  },
};
```

Конфіг вище задає вікно в 600 секунд. Щоб побачити зміну, не чекаючи десять хвилин, я перевизначив його на 15 секунд:

```bash
npx wrangler dev src/rotating.js --port 8788 --var WINDOW_SECONDS:15
```

```bash
$ for i in 1 2 3 4 5; do
    date +%T
    curl -s -D headers.txt localhost:8788/pricing.js | shasum -a 256 | cut -c1-16
    grep -i cache-control headers.txt
    sleep 4
  done
15:03:46
951fdcf8a3cf73ef
Cache-Control: public, max-age=14
15:03:50
951fdcf8a3cf73ef
Cache-Control: public, max-age=10
15:03:54
951fdcf8a3cf73ef
Cache-Control: public, max-age=6
15:03:58
951fdcf8a3cf73ef
Cache-Control: public, max-age=2
15:04:03
4120ce5292b9cf92
Cache-Control: public, max-age=12
```

Ті самі байти всередині вікна, нові після нього, а `max-age` відраховує до межі.

Усе працює завдяки трьом деталям:

- **Той самий seed дає ті самі байти.** Той самий рядок seed, вихідний код і версія пакета давали ідентичний вивід на повторних викликах у моєму тесті. Cache API Cloudflare не реплікується за межі дата-центру, що зберіг запис ([документація Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/cache/)), тож кожна локація збирає свою копію на першому промаху; оскільки seed походить від вікна, усі локації з тією самою розгорнутою версією мають віддавати ті самі байти протягом цього вікна. Seed [не є секретом](https://www.afterpack.dev/docs/builds#random-seed-by-default-pin-for-reproducibility): він лише дає змогу відтворити вивід із вашого вихідного коду, якого ні в кого іншого немає.
- **Браузери отримують час, що залишився, а не збережений.** Влучання в кеш повертається із заголовками, з якими його збережено, тож Worker переписує `cache-control` на виході. Без цього браузер, що потрапив у кінець вікна, тримав би стару версію ще до одного вікна. Слот входить у ключ кешу, тож нове вікно ніколи не читає старий запис.
- **Тестуйте кеш локально або на своєму домені.** Cache API не діє в редакторі дашборду та в превʼю Playground, тож використовуйте `wrangler dev` або розгорнутий маршрут.

## Скільки CPU коштує обфускація у Worker?

Для цього файлу з 16 рядків — 4–12 мс часу запиту після прогріву під `wrangler dev`. Реальний бандл потребує сотень мілісекунд на збірку на `light` і секунд на `hard`, значно більше за ліміт у 10 мс CPU безкоштовного плану Workers.

За пʼять холодних стартів `wrangler dev` на моєму M2 Max перший запит тривав 59–338 мс (разом зі стартом рушія), а наступні встановилися на 4–12 мс. Це час запиту в локальному процесі workerd, а не час CPU в isolate на edge, тож вимірюйте на Cloudflare, перш ніж щось планувати. Для масштабу: документація [виміряла](https://www.afterpack.dev/docs/performance#the-wasm-build) 271 мс на `light` і 1558 мс на `medium` у Node для бандла 185 КБ з AfterPack 0.2.0; це демо використовує `hard`, тобто [складність](https://www.afterpack.dev/docs/complexity) 12 ([пресети](https://www.afterpack.dev/docs/presets)).

Рушій вкладається в ліміт розміру Worker на будь-якому плані. Для всього, що більше за крихітний скрипт, безкоштовний план відпадає через ліміт CPU ([документація](https://www.afterpack.dev/docs/workers)).

## На кожен запит, на вікно чи на збірку?

Обфускуйте на збірку великі бандли та все, що лежить на шляху завантаження сторінки, на вікно — скрипти, які хочете змінювати дешево, а на кожен запит — лише там, де хтось знову і знову запускає ту саму автоматизацію проти вашого скрипта.

| Ритм | Де працює | Збірки | Кешування | Підходить для |
|---|---|---|---|---|
| На збірку | [CLI](https://www.afterpack.dev/docs/cli) або плагін, у CI | Одна на деплой | Ваш CDN, як будь-який файл | Великі бандли, критичний шлях завантаження |
| На вікно | Ваш Worker, seed зі слота часу | Одна на вікно на локацію | Cache API, `max-age` = секунди, що лишилися | Скрипти, які хочете змінювати, не платячи за кожного відвідувача |
| На кожен запит | Ваш Worker, випадковий seed | Одна на запит | `no-store` | Антибот- і challenge-скрипти, перевірки цілісності, скрипти, які патчать скрапери |

## Як запускати обфускацію Pro з Worker

Передайте [`key`](https://www.afterpack.dev/docs/config#key), наприклад `{ key: env.AFTERPACK_KEY }` із секрету Worker, і той самий виклик `obfuscate()` запустить [Pro](https://www.afterpack.dev/docs/pro) у хмарі AfterPack. Пакет, який ви встановлюєте, сам по собі виконує захист Free.

Цей виклик має бути у Worker із ротацією, а не в тому, що працює на кожен запит, де кожен відвідувач чекав би на мережевий обмін із хмарною збіркою. Стежте й за довжиною вікна: Pro [тарифікується за оброблені МБ](https://www.afterpack.dev/docs/pro#what-pro-costs), а кожна локація збирає свою копію раз на вікно. Скрипт на 20 КБ із вікном 10 хвилин у 30 локаціях — це близько 86 МБ на день, 2,6 ГБ на місяць, значно більше за 500 МБ плану Indie. З вікном 6 годин той самий скрипт — близько 72 МБ на місяць.

Кожна хмарна збірка також є записаною збіркою з власною [Protection Map](https://www.afterpack.dev/docs/protection-map), а проєкт зберігає лише останні 500, тож [вимкніть зберігання мап](https://www.afterpack.dev/docs/protection-map#turning-it-off) для проєкту, під яким збирає ваш Worker із ротацією. Ваш вихідний код залишає Worker для цього виклику, обробляється в памʼяті й видаляється ([приватність](https://www.afterpack.dev/docs/privacy)). Якщо хмара недоступна, виклик [завершується закрито](https://www.afterpack.dev/docs/diagnostics#fail-closed-no-exceptions): кидає виняток, і нічого незахищеного не відвантажується. Перехопіть виняток і віддайте версію, збережену з попереднього вікна, інакше відвідувач отримає помилку.

## Спробуйте

Скопіюйте три файли вище (`wrangler.jsonc`, `src/worker.js`, `src/pricing.client.js`), виконайте `npm i @afterpack/wasm wrangler` і `npx wrangler dev`, а потім двічі зробіть curl на `/pricing.js`. Довідник пакета — [Обфускуйте JavaScript у Cloudflare Worker](https://www.afterpack.dev/docs/workers); зберігайте явний `seed` із цього посту, коли адаптуєте код.

## Часті запитання

### Чи можна обфускувати JavaScript на безкоштовному плані Cloudflare Workers?

Лише для крихітних скриптів, і навіть тоді перший запит на isolate (59–338 мс часу запиту під `wrangler dev`, разом зі стартом рушія) може перевищити ліміт у 10 мс CPU безкоштовного плану. Беріть Workers Paid, бажано з кешем на вікно, описаним вище.

### Чи сповільнює обфускація на кожен запит мій сайт?

Для цього скрипта з 16 рядків прогріті запити тривали 4–12 мс під `wrangler dev`, а перший запит на isolate — 59–338 мс. З кешем на вікно збирає лише перший запит у кожному вікні в кожній локації; більшість влучань у кеш поверталися за 2–3 мс. Обфускація на збірку нічого не додає до часу запиту.

### Чи може обфускація на кожен запит зупинити веб-скрапінг і ботів?

Вона ламає хуки, патчі та скрапери, написані під одну відповідь, бо наступна відповідь — інший скрипт. Вона не завадить людині прочитати відповідь і сама по собі не виявляє ботів; вона робить дорожчою автоматизацію проти скриптів, на які спираються ваші антибот-перевірки.

### Чи ховає обфускація JavaScript на кожен запит API-ключі?

Ні. Усе, що потрібно браузеру, браузер може прочитати, хоч би в якій формі воно прийшло. Тримайте секрети й рішення на кшталт фінальної ціни чи дійсності купона на сервері, як це робить демо. [Як захистити вихідний код JavaScript](https://www.afterpack.dev/blog/protect-javascript-source-code) показує, як перевірити, що ваш сайт уже відвантажує.

### Чи може AfterPack віддавати обфускацію на кожен запит без мого власного Worker?

Ні. Worker вище, у вашому власному обліковому записі, і є інтеграцією. Для Pro покладіть свій ключ у той самий виклик `obfuscate()`: збірка виконується в хмарі AfterPack на рушії Pro, а файл, завеликий для Worker, автоматично переходить у контейнер. Складніше питання — який із ваших скриптів справді має когось, хто щодня запускає проти нього ту саму автоматизацію.
