Щоб обфускувати 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, який ви відвантажуєте: ШІ-агенту вистачило кількох хвилин, щоб перетворити обфусковані демо назад на чистий вихідний код. CLI AfterPack і плагіни для фреймворків відповідають на це на рівні релізу: новий seed на кожну збірку (пост про запуск). У вашому власному Worker той самий рушій переносить цей ритм на кожен запит, тож будь-яку автоматизацію, написану під одну копію скрипта, доводиться переробляти під наступну.
Що зупиняє обфускація коду на кожен запит?
Хуки, патчі та скрипти деобфускації, зібрані під одну відповідь, перестають працювати на наступній, бо ідентифікатори, зсуви й позиції декодера щоразу рухаються. Це важливо для скриптів, до яких автоматизація повертається знову і знову: антибот- і challenge-скриптів, клієнтських перевірок цілісності та скриптів, які скрапери патчать або перехоплюють, щоб їх обійти.
Кожна відповідь до того ж є повною збіркою AfterPack: алгоритми переписано, потік керування сплющено, ідентичності злито, тож навіть відновлена копія відрізняється від коду, який ви написали. Проте всі відповіді залишаються однією програмою, і жоден обфускатор, зокрема AfterPack, не робить відновлення неможливим.
Як встановити обфускатор 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.
| npm i @afterpack/wasm wrangler |
@afterpack/wasm — це той самий рушій на Rust, що працює в CLI і плагінах, скомпільований у WebAssembly; мапа exports пакета дає wrangler точку входу у формі Worker (документація). Той самий імпорт працює в Node, де інша точка входу читає бінарник із диска, хоча у звичайній збірці на Node нативний @afterpack/core швидший.
Скрипт у демо — невеликий клієнтський калькулятор цін. Він заміняє challenge-скрипт або скрипт перевірки цілісності і достатньо короткий, щоб звірити вивід вручну:
src/pricing.client.js — вихідний код, який віддається
| 1 | const TIERS = [ |
| 2 | { min: 1, max: 9, unit: 12 }, |
| 3 | { min: 10, max: 49, unit: 10 }, |
| 4 | { min: 50, max: Infinity, unit: 8 }, |
| 5 | ]; |
| 6 | |
| 7 | function unitPrice(quantity) { |
| 8 | const tier = TIERS.find((t) => quantity >= t.min && quantity <= t.max); |
| 9 | return tier ? tier.unit : TIERS[0].unit; |
| 10 | } |
| 11 | |
| 12 | export function quote(quantity, coupon) { |
| 13 | let total = unitPrice(quantity) * quantity; |
| 14 | if (coupon === "LAUNCH20" && quantity >= 10) total *= 0.8; |
| 15 | return Math.round(total * 100) / 100; |
| 16 | } |
Worker імпортує цей файл як рядок, для чого потрібне одне правило wrangler для вихідного файлу (рушію воно не потрібне). wrangler.jsonc:
| 1 | { |
| 2 | "name": "per-request-obfuscation", |
| 3 | "main": "src/worker.js", |
| 4 | "compatibility_date": "2026-09-25", |
| 5 | "vars": { "WINDOW_SECONDS": "600" }, |
| 6 | "rules": [{ "type": "Text", "globs": ["**/*.client.js"], "fallthrough": true }] |
| 7 | } |
Як обфускувати JavaScript на кожен запит
Згенеруйте випадковий seed в обробнику fetch, передайте його в obfuscate() і поверніть результат із cache-control: no-store. Тоді кожна відповідь збирається зі свого власного seed.
src/worker.js:
| 1 | import { obfuscate } from "@afterpack/wasm"; |
| 2 | import source from "./pricing.client.js"; |
| 3 | |
| 4 | export default { |
| 5 | async fetch(request) { |
| 6 | if (new URL(request.url).pathname !== "/pricing.js") { |
| 7 | return new Response("Not found", { status: 404 }); |
| 8 | } |
| 9 | const seed = crypto.getRandomValues(new Uint32Array(1))[0]; |
| 10 | const result = await obfuscate({ path: "pricing.js", source }, { preset: "hard", seed }); |
| 11 | return new Response(result.bytes, { |
| 12 | headers: { |
| 13 | "content-type": "application/javascript", |
| 14 | "cache-control": "no-store", |
| 15 | }, |
| 16 | }); |
| 17 | }, |
| 18 | }; |
У Worker завжди передавайте seed самі: саме випадковий seed із crypto.getRandomValues робить кожну відповідь іншою. cache-control: no-store це зберігає, бо браузер, інший CDN перед вашим доменом або корпоративний проксі інакше могли б залишити собі першу версію, і робота на кожен запит нічого б не дала. Щоб відтворити відповідь пізніше, логуйте її seed або повертайте його в заголовку відповіді; seed не є секретом.
Запустіть npx wrangler dev і двічі запитайте ту саму URL:
| 1 | $ for i in 1 2; do curl -s localhost:8787/pricing.js | shasum -a 256; done |
| 2 | 0265ae6f5cf5a0839ee52a9a08bab6326503849c66296ae581c2c21923d2d676 - |
| 3 | a5684abbb2ba13be3c182840d30b1f2a634b241013e112be70c6de22ac1233b4 - |
| 4 | |
| 5 | $ for i in 1 2; do curl -s localhost:8787/pricing.js | head -c 100; echo; done |
| 6 | 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) |
| 7 | 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 |
| 8 | |
| 9 | $ for i in 1 2; do curl -s localhost:8787/pricing.js | wc -c; done |
| 10 | 3823 |
| 11 | 3545 |
Хеші, ідентифікатори й довжини різняться, і жодне тіло не містить TIERS чи unitPrice. Я зберіг дві відповіді як модулі й викликав quote() у кожній поруч з оригіналом для шести пар кількість/купон; усі три повернули ті самі суми. Рядок купона всередині лишається, закодований і декодований під час виконання, і це нормально: сервер, що приймає замовлення, перераховує ціну й перевіряє купон, а ця клієнтська копія лише показує розрахунок.
Як кешувати обфускований JavaScript і змінювати його кожні 10 хвилин
Виводьте seed із поточного вікна часу замість випадкового і кешуйте результат за ключем, що містить вікно. Тоді кожна локація збирає одну обфусковану версію на вікно і віддає її до кінця вікна, тож відвідувачі більше не платять CPU кожен за свою збірку:
src/rotating.js — одна версія на вікно, з кешем
| 1 | import { obfuscate } from "@afterpack/wasm"; |
| 2 | import source from "./pricing.client.js"; |
| 3 | |
| 4 | export default { |
| 5 | async fetch(request, env, ctx) { |
| 6 | const url = new URL(request.url); |
| 7 | if (url.pathname !== "/pricing.js") { |
| 8 | return new Response("Not found", { status: 404 }); |
| 9 | } |
| 10 | |
| 11 | const windowSeconds = Number(env.WINDOW_SECONDS); |
| 12 | const now = Math.floor(Date.now() / 1000); |
| 13 | const slot = Math.floor(now / windowSeconds); |
| 14 | const secondsLeft = (slot + 1) * windowSeconds - now; |
| 15 | const cacheKey = new Request(`${url.origin}/pricing.js?slot=${slot}`); |
| 16 | |
| 17 | let response = await caches.default.match(cacheKey); |
| 18 | if (!response) { |
| 19 | const result = await obfuscate( |
| 20 | { path: "pricing.js", source }, |
| 21 | { preset: "hard", seed: `pricing.js:${slot}` }, |
| 22 | ); |
| 23 | response = new Response(result.bytes, { |
| 24 | headers: { |
| 25 | "content-type": "application/javascript", |
| 26 | "cache-control": `public, max-age=${secondsLeft}`, |
| 27 | }, |
| 28 | }); |
| 29 | ctx.waitUntil(caches.default.put(cacheKey, response.clone())); |
| 30 | } |
| 31 | |
| 32 | response = new Response(response.body, response); |
| 33 | response.headers.set("cache-control", `public, max-age=${secondsLeft}`); |
| 34 | return response; |
| 35 | }, |
| 36 | }; |
Конфіг вище задає вікно в 600 секунд. Щоб побачити зміну, не чекаючи десять хвилин, я перевизначив його на 15 секунд:
| npx wrangler dev src/rotating.js --port 8788 --var WINDOW_SECONDS:15 |
| 1 | $ for i in 1 2 3 4 5; do |
| 2 | date +%T |
| 3 | curl -s -D headers.txt localhost:8788/pricing.js | shasum -a 256 | cut -c1-16 |
| 4 | grep -i cache-control headers.txt |
| 5 | sleep 4 |
| 6 | done |
| 7 | 15:03:46 |
| 8 | 951fdcf8a3cf73ef |
| 9 | Cache-Control: public, max-age=14 |
| 10 | 15:03:50 |
| 11 | 951fdcf8a3cf73ef |
| 12 | Cache-Control: public, max-age=10 |
| 13 | 15:03:54 |
| 14 | 951fdcf8a3cf73ef |
| 15 | Cache-Control: public, max-age=6 |
| 16 | 15:03:58 |
| 17 | 951fdcf8a3cf73ef |
| 18 | Cache-Control: public, max-age=2 |
| 19 | 15:04:03 |
| 20 | 4120ce5292b9cf92 |
| 21 | Cache-Control: public, max-age=12 |
Ті самі байти всередині вікна, нові після нього, а max-age відраховує до межі.
Усе працює завдяки трьом деталям:
- Той самий seed дає ті самі байти. Той самий рядок seed, вихідний код і версія пакета давали ідентичний вивід на повторних викликах у моєму тесті. Cache API Cloudflare не реплікується за межі дата-центру, що зберіг запис (документація Cloudflare), тож кожна локація збирає свою копію на першому промаху; оскільки seed походить від вікна, усі локації з тією самою розгорнутою версією мають віддавати ті самі байти протягом цього вікна. Seed не є секретом: він лише дає змогу відтворити вивід із вашого вихідного коду, якого ні в кого іншого немає.
- Браузери отримують час, що залишився, а не збережений. Влучання в кеш повертається із заголовками, з якими його збережено, тож 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, перш ніж щось планувати. Для масштабу: документація виміряла 271 мс на light і 1558 мс на medium у Node для бандла 185 КБ; це демо використовує hard, тобто складність 12 (пресети).
Рушій вкладається в ліміт розміру Worker на будь-якому плані. Для всього, що більше за крихітний скрипт, безкоштовний план відпадає через ліміт CPU (документація).
На кожен запит, на вікно чи на збірку?
Обфускуйте на збірку великі бандли та все, що лежить на шляху завантаження сторінки, на вікно — скрипти, які хочете змінювати дешево, а на кожен запит — лише там, де хтось знову і знову запускає ту саму автоматизацію проти вашого скрипта.
| Ритм | Де працює | Збірки | Кешування | Підходить для |
|---|---|---|---|---|
| На збірку | CLI або плагін, у CI | Одна на деплой | Ваш CDN, як будь-який файл | Великі бандли, критичний шлях завантаження |
| На вікно | Ваш Worker, seed зі слота часу | Одна на вікно на локацію | Cache API, max-age = секунди, що лишилися | Скрипти, які хочете змінювати, не платячи за кожного відвідувача |
| На кожен запит | Ваш Worker, випадковий seed | Одна на запит | no-store | Антибот- і challenge-скрипти, перевірки цілісності, скрипти, які патчать скрапери |
Як запускати обфускацію Pro з Worker
Передайте key, наприклад { key: env.AFTERPACK_KEY } із секрету Worker, і той самий виклик obfuscate() запустить Pro у хмарі AfterPack. Пакет, який ви встановлюєте, сам по собі виконує захист Free.
Цей виклик має бути у Worker із ротацією, а не в тому, що працює на кожен запит, де кожен відвідувач чекав би на мережевий обмін із хмарною збіркою. Стежте й за довжиною вікна: Pro тарифікується за оброблені МБ, а кожна локація збирає свою копію раз на вікно. Скрипт на 20 КБ із вікном 10 хвилин у 30 локаціях — це близько 86 МБ на день, 2,6 ГБ на місяць, значно більше за 500 МБ плану Indie. З вікном 6 годин той самий скрипт — близько 72 МБ на місяць.
Кожна хмарна збірка також є записаною збіркою з власною Protection Map, а проєкт зберігає лише останні 500, тож вимкніть зберігання мап для проєкту, під яким збирає ваш Worker із ротацією. Ваш вихідний код залишає Worker для цього виклику, обробляється в памʼяті й видаляється (приватність). Якщо хмара недоступна, виклик завершується закрито: кидає виняток, і нічого незахищеного не відвантажується. Перехопіть виняток і віддайте версію, збережену з попереднього вікна, інакше відвідувач отримає помилку.
Спробуйте
Скопіюйте три файли вище (wrangler.jsonc, src/worker.js, src/pricing.client.js), виконайте npm i @afterpack/wasm wrangler і npx wrangler dev, а потім двічі зробіть curl на /pricing.js. Довідник пакета — Обфускуйте JavaScript у Cloudflare Worker; зберігайте явний 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 показує, як перевірити, що ваш сайт уже відвантажує.
Чи може AfterPack віддавати обфускацію на кожен запит без мого власного Worker?
Поки що ні. Це в roadmap, ще не випущено. До того часу Worker вище — це вся інтеграція, а складніше питання — який із ваших скриптів справді має когось, хто щодня запускає проти нього ту саму автоматизацію.
