AfterPack ब्लॉग
guidesecurity

Cloudflare Workers में हर रिक्वेस्ट पर JavaScript ओबफस्केट करें

द्वारा Nikita Savchenko10 मिनट पढ़ने का समय

Cloudflare Worker में हर रिक्वेस्ट पर JavaScript ओबफस्केट (obfuscate) करने के लिए @afterpack/wasm इंस्टॉल करें और अपने fetch हैंडलर में obfuscate() को रैंडम seed के साथ कॉल करें: हर रिस्पॉन्स संरचना में अलग स्क्रिप्ट होती है जिसका व्यवहार एक जैसा रहता है। यह पैकेज AfterPack (afterpack.dev), यानी JavaScript obfuscator, का इंजन है जो WebAssembly में कंपाइल किया गया है; यह मुफ़्त है और आपके Worker के अंदर चलता है। असली bundle फ़्री प्लान की 10 ms CPU सीमा से ज़्यादा लेते हैं, इसलिए Workers Paid इस्तेमाल करें और हर टाइम विंडो का एक ओबफस्केटेड वर्ज़न कैश करें।

नीचे की हर चीज़ मैंने wrangler dev के साथ चलाई: बिल्कुल वही Worker, curl आउटपुट जिसमें एक ही URL के लिए दो अलग बॉडी दिखती हैं, और एक कैश किया हुआ वेरिएंट जो हर रिक्वेस्ट के बजाय हर दस मिनट में बदलता है। हर रिक्वेस्ट पर कोड ओबफस्केशन के लिए एक npm पैकेज और बीस से कम लाइनों का Worker कोड काफ़ी है।

AI ने शिप किए गए JavaScript को पढ़ना सस्ता कर दिया है: एक AI एजेंट को ओबफस्केट किए गए डेमो वापस साफ़ सोर्स में बदलने में बस कुछ मिनट लगे। AfterPack CLI और framework plugins इसका जवाब हर रिलीज़ पर देते हैं: हर बिल्ड पर नया seed (लॉन्च पोस्ट)। आपके अपने Worker के अंदर वही इंजन यह रफ़्तार हर रिक्वेस्ट तक ले जाता है, इसलिए किसी स्क्रिप्ट की एक कॉपी के ख़िलाफ़ बनाई गई कोई भी ऑटोमेशन अगली कॉपी के लिए दोबारा बनानी पड़ती है।

हर रिक्वेस्ट पर कोड ओबफस्केशन क्या रोकता है?

एक रिस्पॉन्स के ख़िलाफ़ बनाए गए hook, patch और deobfuscation स्क्रिप्ट अगले रिस्पॉन्स पर काम करना बंद कर देते हैं, क्योंकि identifiers, offsets और decoder की पोज़िशन हर बार बदलती हैं। यह उन स्क्रिप्ट के लिए मायने रखता है जिन पर ऑटोमेशन बार-बार लौटता है: anti-bot और challenge स्क्रिप्ट, क्लाइंट-साइड integrity checks, और वे स्क्रिप्ट जिन्हें scrapers बायपास करने के लिए patch या hook करते हैं।

हर रिस्पॉन्स एक पूरा AfterPack बिल्ड भी होता है: algorithms दोबारा लिखे जाते हैं, control flow फ़्लैट किया जाता है और identities मिला दी जाती हैं, इसलिए रिवर्स की गई कॉपी भी आपके लिखे कोड से अलग होती है। फिर भी हर रिस्पॉन्स वही प्रोग्राम है, और कोई भी obfuscator, AfterPack समेत, रिवर्सल को असंभव नहीं बनाता।

Cloudflare Worker में JavaScript obfuscator कैसे इंस्टॉल करें

wrangler के साथ @afterpack/wasm इंस्टॉल करें और उससे obfuscate इम्पोर्ट करें। कोई init कॉल नहीं, कोई nodejs_compat फ़्लैग नहीं और .wasm फ़ाइल के लिए कोई wrangler rule नहीं।

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 और plugins चलाते हैं, WebAssembly में कंपाइल किया हुआ; पैकेज का exports मैप wrangler को Worker के लिए बनी एंट्री देता है (डॉक्स)। यही import Node में भी काम करता है, जहाँ एक दूसरी एंट्री बाइनरी को डिस्क से पढ़ती है, हालाँकि सामान्य Node बिल्ड में नेटिव @afterpack/core ज़्यादा तेज़ है।

डेमो स्क्रिप्ट एक छोटा क्लाइंट-साइड प्राइस कैलकुलेटर है। यह किसी challenge या integrity स्क्रिप्ट की जगह खड़ी है, और इतनी छोटी है कि आउटपुट को हाथ से जाँचा जा सके:

src/pricing.client.js — जो सोर्स सर्व किया जा रहा है
1const TIERS = [
2 { min: 1, max: 9, unit: 12 },
3 { min: 10, max: 49, unit: 10 },
4 { min: 50, max: Infinity, unit: 8 },
5];
6
7function unitPrice(quantity) {
8 const tier = TIERS.find((t) => quantity >= t.min && quantity <= t.max);
9 return tier ? tier.unit : TIERS[0].unit;
10}
11
12export 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 इस फ़ाइल को string के रूप में इम्पोर्ट करता है, जिसके लिए सोर्स फ़ाइल का एक wrangler rule चाहिए (इंजन को कोई नहीं चाहिए)। 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 कैसे ओबफस्केट करें

fetch हैंडलर में एक रैंडम seed बनाएँ, उसे obfuscate() को दें, और नतीजा cache-control: no-store के साथ लौटाएँ। तब हर रिस्पॉन्स अपने ही seed से बनता है।

src/worker.js:

1import { obfuscate } from "@afterpack/wasm";
2import source from "./pricing.client.js";
3
4export 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 हमेशा ख़ुद पास करें: crypto.getRandomValues से आया रैंडम seed ही हर रिस्पॉन्स को अलग बनाता है। 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
20265ae6f5cf5a0839ee52a9a08bab6326503849c66296ae581c2c21923d2d676 -
3a5684abbb2ba13be3c182840d30b1f2a634b241013e112be70c6de22ac1233b4 -
4
5$ for i in 1 2; do curl -s localhost:8787/pricing.js | head -c 100; echo; done
6var 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)
7var 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

हैश, identifiers और लंबाई सब अलग हैं, और किसी भी बॉडी में TIERS या unitPrice नहीं है। मैंने दो रिस्पॉन्स को मॉड्यूल की तरह सेव किया और छह quantity/coupon जोड़ों के लिए हर एक पर, मूल कोड के साथ, quote() कॉल किया; तीनों ने एक जैसे टोटल लौटाए। coupon string अब भी अंदर है, रनटाइम पर एन्कोड और डिकोड होती हुई, और यह ठीक है: ऑर्डर लेने वाला सर्वर क़ीमत दोबारा गिनता है और coupon जाँचता है, और यह क्लाइंट कॉपी सिर्फ़ quote दिखाती है।

ओबफस्केटेड JavaScript को कैश करके हर 10 मिनट में कैसे बदलें

रैंडम seed बनाने के बजाय उसे मौजूदा टाइम विंडो से निकालें, और नतीजे को ऐसी key के तहत कैश करें जिसमें विंडो शामिल हो। तब हर लोकेशन हर विंडो के लिए एक ओबफस्केटेड वर्ज़न बनाती है और विंडो ख़त्म होने तक वही सर्व करती है, इसलिए विज़िटरों को अपने-अपने नए बिल्ड का CPU नहीं देना पड़ता:

src/rotating.js — हर विंडो के लिए एक वर्ज़न, कैश किया हुआ
1import { obfuscate } from "@afterpack/wasm";
2import source from "./pricing.client.js";
3
4export 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
715:03:46
8951fdcf8a3cf73ef
9Cache-Control: public, max-age=14
1015:03:50
11951fdcf8a3cf73ef
12Cache-Control: public, max-age=10
1315:03:54
14951fdcf8a3cf73ef
15Cache-Control: public, max-age=6
1615:03:58
17951fdcf8a3cf73ef
18Cache-Control: public, max-age=2
1915:04:03
204120ce5292b9cf92
21Cache-Control: public, max-age=12

विंडो के अंदर वही बाइट्स, उसके बाद नए बाइट्स, और max-age सीमा तक उल्टी गिनती करता है।

तीन बातें इसे काम करने लायक बनाती हैं:

  • एक ही seed से एक ही बाइट्स। मेरे टेस्ट में एक ही seed string, सोर्स और पैकेज वर्ज़न ने बार-बार कॉल करने पर एक जैसा आउटपुट दिया। Cloudflare का Cache API उस डेटा सेंटर के बाहर रेप्लिकेट नहीं होता जिसने एंट्री सेव की (Cloudflare डॉक्स), इसलिए हर लोकेशन अपने पहले miss पर अपनी कॉपी बनाती है; चूँकि seed विंडो से आता है, एक ही डिप्लॉय किया गया वर्ज़न चला रही हर लोकेशन को उस विंडो में वही बाइट्स सर्व करने चाहिए। seed कोई सीक्रेट नहीं है: इससे बस आपके सोर्स से आउटपुट दोबारा बनाया जा सकता है, और आपका सोर्स किसी और के पास नहीं है।
  • ब्राउज़र को बचा हुआ समय मिलता है, सेव किया हुआ नहीं। कैश hit उन्हीं हेडरों के साथ लौटता है जिनके साथ वह सेव हुआ था, इसलिए Worker बाहर जाते समय cache-control दोबारा लिखता है। इसके बिना, विंडो के आख़िर में hit करने वाला ब्राउज़र पुराना वर्ज़न एक और विंडो तक रख सकता था। slot कैश key में है, इसलिए नई विंडो कभी पुरानी एंट्री नहीं पढ़ती।
  • कैशिंग लोकल या अपने डोमेन पर टेस्ट करें। Cache API का डैशबोर्ड एडिटर या Playground प्रीव्यू में कोई असर नहीं होता, इसलिए wrangler dev या डिप्लॉय किया गया route इस्तेमाल करें।

Worker में ओबफस्केशन पर कितना CPU लगता है?

इस 16 लाइन की फ़ाइल के लिए, wrangler dev में वॉर्म होने के बाद 4–12 ms का रिक्वेस्ट टाइम। असली bundle हर बिल्ड पर light में सैकड़ों मिलीसेकंड और hard में कई सेकंड लेता है, जो Workers फ़्री प्लान की 10 ms CPU सीमा से कहीं ज़्यादा है।

मेरे M2 Max पर wrangler dev के पाँच कोल्ड स्टार्ट में पहली रिक्वेस्ट ने 59–338 ms लिए (इंजन स्टार्ट-अप समेत) और बाद वाली 4–12 ms पर टिक गईं। ये लोकल workerd प्रोसेस के रिक्वेस्ट टाइम हैं, edge isolate का CPU टाइम नहीं, इसलिए कुछ भी तय करने से पहले Cloudflare पर मापें। पैमाने के लिए, डॉक्स ने Node में 185 KB के bundle के लिए light पर 271 ms और medium पर 1,558 ms मापे; यह डेमो hard चलाता है, जो complexity 12 है (presets)।

इंजन दोनों प्लान पर Worker की साइज़ सीमा में फ़िट होता है। किसी छोटी-सी स्क्रिप्ट से बड़ी चीज़ के लिए फ़्री प्लान को उसकी CPU सीमा बाहर कर देती है (डॉक्स)।

हर रिक्वेस्ट पर, हर विंडो पर या हर बिल्ड पर?

बड़े bundle और पेज-लोड पाथ की हर चीज़ हर बिल्ड पर ओबफस्केट करें, जिन स्क्रिप्ट को सस्ते में बदलना है उन्हें हर विंडो पर, और हर रिक्वेस्ट पर सिर्फ़ वहाँ जहाँ कोई आपकी स्क्रिप्ट के ख़िलाफ़ बार-बार वही ऑटोमेशन चलाता है।

रफ़्तारकहाँ चलता हैबिल्डकैशिंगकिसके लिए
हर बिल्ड परCLI या plugin, CI मेंहर डिप्लॉय पर एकआपका CDN, किसी भी फ़ाइल की तरहबड़े bundle, पेज-लोड का hot path
हर विंडो परआपका Worker, टाइम slot से seedहर विंडो हर लोकेशन पर एकCache API, max-age = बचे हुए सेकंडवे स्क्रिप्ट जिन्हें हर विज़िटर का ख़र्च दिए बिना बदलना है
हर रिक्वेस्ट परआपका Worker, रैंडम seedहर रिक्वेस्ट पर एकno-storeAnti-bot और challenge स्क्रिप्ट, integrity checks, वे स्क्रिप्ट जिन्हें scrapers patch करते हैं

Worker से Pro ओबफस्केशन कैसे चलाएँ

एक key पास करें, जैसे Worker secret से { key: env.AFTERPACK_KEY }, और वही obfuscate() कॉल AfterPack के क्लाउड में Pro चलाती है। आप जो पैकेज इंस्टॉल करते हैं वह अपने आप Free सुरक्षा देता है।

यह कॉल rotation Worker में होनी चाहिए, हर रिक्वेस्ट वाले Worker में नहीं, जहाँ हर विज़िटर को क्लाउड बिल्ड के राउंड ट्रिप का इंतज़ार करना पड़ता। विंडो की लंबाई पर भी नज़र रखें: Pro का बिल प्रोसेस किए गए MB के हिसाब से बनता है, और हर लोकेशन हर विंडो में अपनी कॉपी बनाती है। 20 KB की स्क्रिप्ट, 10 मिनट की विंडो और 30 लोकेशन पर रोज़ लगभग 86 MB बनती है, महीने में 2.6 GB, जो Indie के 500 MB से कहीं ज़्यादा है। 6 घंटे की विंडो पर वही स्क्रिप्ट महीने में लगभग 72 MB बनती है।

हर क्लाउड बिल्ड एक रिकॉर्ड किया गया बिल्ड भी है जिसकी अपनी Protection Map होती है, और एक प्रोजेक्ट सिर्फ़ आख़िरी 500 रखता है, इसलिए जिस प्रोजेक्ट के तहत आपका rotation Worker बिल्ड करता है उसके लिए map स्टोरेज बंद कर दें। इस कॉल के लिए आपका सोर्स Worker से बाहर जाता है, मेमोरी में प्रोसेस होता है और हटा दिया जाता है (प्राइवेसी)। अगर क्लाउड तक नहीं पहुँचा जा सकता, तो कॉल fail closed होती है: वह throw करती है और कुछ भी असुरक्षित शिप नहीं होता। उस throw को पकड़ें और किसी पिछली विंडो से रखा वर्ज़न सर्व करें, वरना विज़िटर को एरर मिलेगा।

आज़माकर देखें

ऊपर की तीन फ़ाइलें (wrangler.jsonc, src/worker.js, src/pricing.client.js) कॉपी करें, npm i @afterpack/wasm wrangler और npx wrangler dev चलाएँ, फिर /pricing.js को दो बार curl करें। पैकेज का रेफ़रेंस Cloudflare Worker के अंदर JavaScript ओबफस्केट करें है; इसे अपनाते समय इस पोस्ट का explicit seed बनाए रखें।

अक्सर पूछे जाने वाले सवाल

क्या मैं Cloudflare Workers फ़्री प्लान पर JavaScript ओबफस्केट कर सकता हूँ?

सिर्फ़ बहुत छोटी स्क्रिप्ट के लिए, और तब भी हर isolate की पहली रिक्वेस्ट (wrangler dev में 59–338 ms का रिक्वेस्ट टाइम, इंजन स्टार्ट-अप समेत) फ़्री प्लान की 10 ms CPU सीमा पार कर सकती है। Workers Paid इस्तेमाल करें, बेहतर हो तो ऊपर वाले हर-विंडो कैश के साथ।

क्या हर रिक्वेस्ट पर ओबफस्केशन मेरी साइट को धीमा करता है?

इस 16 लाइन की स्क्रिप्ट के लिए, wrangler dev में वॉर्म रिक्वेस्ट ने 4–12 ms लिए, और हर isolate की पहली रिक्वेस्ट ने 59–338 ms। हर-विंडो कैश के साथ सिर्फ़ हर लोकेशन पर हर विंडो की पहली रिक्वेस्ट बिल्ड करती है; ज़्यादातर कैश hit 2–3 ms में लौटे। हर बिल्ड पर ओबफस्केशन रिक्वेस्ट टाइम में कुछ नहीं जोड़ता।

क्या हर रिक्वेस्ट पर ओबफस्केशन वेब स्क्रेपिंग और बॉट्स को रोक सकता है?

यह एक रिस्पॉन्स के ख़िलाफ़ लिखे गए hooks, patches और scrapers को तोड़ देता है, क्योंकि अगला रिस्पॉन्स एक अलग स्क्रिप्ट है। यह किसी इंसान को रिस्पॉन्स पढ़ने से नहीं रोकता, और अपने आप बॉट्स का पता नहीं लगाता; यह उन स्क्रिप्ट के ख़िलाफ़ ऑटोमेशन की लागत बढ़ाता है जिन पर आपके bot checks टिके हैं।

क्या हर रिक्वेस्ट पर JavaScript ओबफस्केट करने से API keys छिप जाती हैं?

नहीं। ब्राउज़र को जो भी चाहिए, ब्राउज़र उसे पढ़ सकता है, चाहे वह किसी भी रूप में आए। सीक्रेट्स और अंतिम क़ीमत या coupon की वैधता जैसे फ़ैसले सर्वर पर रखें, जैसा डेमो करता है। JavaScript सोर्स कोड कैसे सुरक्षित करें बताता है कि आपकी साइट पहले से क्या शिप कर रही है, यह कैसे जाँचें।

क्या AfterPack मेरे अपने Worker के बिना हर रिक्वेस्ट पर ओबफस्केशन दे सकता है?

अभी नहीं। यह roadmap पर है, शिप नहीं हुआ। तब तक ऊपर वाला Worker ही पूरा इंटीग्रेशन है, और असली कठिन सवाल यह है कि आपकी कौन-सी स्क्रिप्ट के ख़िलाफ़ सच में कोई हर दिन वही ऑटोमेशन चला रहा है।

अपडेट रहें

रिलीज़ नोट्स और लेखों के लिए AfterPack को फॉलो करें।