Щоб захистити вихідний код JavaScript, перестаньте віддавати source maps, тримайте секрети й перевірки доступу на сервері та обфускуйте код, який мусить потрапити в браузер. Кожен відвідувач завантажує ваш бандл, а мініфікація лише скорочує локальні імена: рядки, API-маршрути, feature flags і правила ціноутворення лишаються читабельними. AfterPack (afterpack.dev), обфускатор JavaScript, перетворює ваш бандл на структурно іншу програму з кожною збіркою. Його CLI з відкритим кодом працює безкоштовно й локально через npx afterpack; Pro додає директиви для окремих ділянок коду та anti-tamper.
Щоб показати, як це виглядає, я зібрав невеликий демо-застосунок: чотири TypeScript-файли з ціною за місце, промокодами, feature flags, лімітом експорту на безкоштовному тарифі та двома викликами API. Забандлив його esbuild 0.28.1 (--bundle --minify --sourcemap) 27.09.2026. Усе нижче — реальний вивід цієї збірки, а ті самі перевірки на власному продакшн-сайті займуть близько п'яти хвилин: DevTools, один curl і безкоштовний сканер безпеки.
Чи можуть користувачі побачити ваш JavaScript-код?
Так. Кожен скрипт, який запускає сторінка, завантажується в браузер відвідувача, і DevTools показує їх усі.
«Перегляд коду сторінки» (Ctrl+U, або Cmd+Option+U на macOS) показує лише HTML і теги скриптів; DevTools показує самі скрипти. Застосунки на React, Next.js і Vue відвантажуються так само: ваші компоненти приходять як JavaScript-файли, які може відкрити будь-хто.
Відкрийте свій продакшн-сайт, а не localhost. У Chrome відкрийте DevTools (Cmd+Option+I на macOS, F12 деінде) і перейдіть на вкладку Sources. Ваші бандли перелічені під вашим доменом. Клацніть на один. Якщо він ще не відформатований, натисніть кнопку {} внизу.
Тепер шукайте по всіх завантажених файлах через Cmd+Option+F (Ctrl+Shift+F на Windows і Linux). Спробуйте /api, /v1, internal, admin, free, pro, trial, promo і назву будь-якої функції, яку ви ще не анонсували.
Що видає мініфікований JavaScript?
Усе, крім локальних імен. Увесь мініфікований бандл мого демо займає 995 байтів:
| (()=>{var f={LAUNCH40:.4,PARTNER_FRIENDS:.5};function i(e,t,o){let r=e*1200;e>=25&&(r*=.9),t&&(r*=1-.2);let p=o?f[o.toUpperCase()]??0:0;return r*=1-p,Math.round(r)}function a(e,t){return e==="free"?t<3:!0}var l={newCheckout:!0,aiSummaries:!1,teamSeatsBeta:!0};function s(e,t){return e==="aiSummaries"?t==="pro"||t==="team":l[e]}var u="https://api.example.com";async function c(e){return(await fetch(`${u}/v2/billing/invoices`,{headers:{Authorization:`Bearer ${e}`}})).json()}async function m(e){return fetch(`${u}/internal/admin/trials/${e}/extend`,{method:"POST"})}var n={id:"u_1",plan:"free",exportsThisMonth:2};document.querySelector("#price").textContent=String(i(10,!0,"launch40")/100);document.querySelector("#export").addEventListener("click",()=>{if(!a(n.plan,n.exportsThisMonth)){alert("Upgrade to Pro to export more than 3 files a month");return}s("aiSummaries",n.plan)&&console.log("summaries on")});window.__debug={fetchInvoices:c,grantTrial:m};})(); |
| //# sourceMappingURL=app.js.map |
Локальних імен немає; все інше на місці, включно з іменами, які я навмисно поклав у window, як-от grantTrial:
| Що було у вихідному коді | Читабельне після мініфікації? | Де це в бандлі |
|---|---|---|
| Локальні імена функцій і змінних | Ні, скорочені | quote() став i() |
| Промокоди та їхні знижки | Так | LAUNCH40:.4,PARTNER_FRIENDS:.5 |
| Ціна за місце в центах і знижка від 25 місць | Так | e*1200, e>=25&&(r*=.9) |
| Ліміт у три експорти на безкоштовному тарифі | Так | e==="free"?t<3:!0 |
| Усі три feature flags | Так | newCheckout:!0,aiSummaries:!1,teamSeatsBeta:!0 |
| API-хост і маршрути, включно з адмінським | Так | /v2/billing/invoices, /internal/admin/trials/${e}/extend |
Імена, прикріплені до window | Так | window.__debug={fetchInvoices:c,grantTrial:m} |
| Оригінальні файли, типи й імена констант | Лише якщо source map публічний | рядок sourceMappingURL |
Мініфікація робить файли меншими; ховати щось вона ніколи не мала.
Чи небезпечні source maps у продакшні?
Так, якщо мапа публічна. Source map зазвичай містить ваші оригінальні файли, тож віддаючи її, ви публікуєте вихідний код.
Подивіться на останній рядок кожного бандла. Коментар на кшталт //# sourceMappingURL=app.js.map підказує браузеру, де лежить мапа. Деякі конфігурації передають її заголовком відповіді SourceMap, тож гляньте й на вкладку Network.
Потім запитайте мапу самі й перевірте тіло відповіді, а не статус-код. Багато хостингів на будь-який невідомий шлях відповідають 200 і вашим index.html. Якщо команда виводить 3, ви віддаєте source map:
| curl -s https://example.com/assets/app.js.map | jq .version |
| # 3 (my demo, served locally; an HTML page gives a jq parse error instead) |
Source map зазвичай містить масив sourcesContent, необов'язкове поле формату source map: ваші оригінальні файли, рівно такими, як ви їх написали. Два виклики jq перелічують їх і друкують один:
| 1 | curl -s https://example.com/assets/app.js.map | jq -r '.sources[]' |
| 2 | # ../src/pricing.ts |
| 3 | # ../src/flags.ts |
| 4 | # ../src/api.ts |
| 5 | # ../src/app.ts |
| 6 | |
| 7 | curl -s https://example.com/assets/app.js.map | jq -r '.sourcesContent[0]' | head -8 |
| 8 | # const SEAT_PRICE_CENTS = 1200; |
| 9 | # const ANNUAL_DISCOUNT = 0.2; |
| 10 | # const PROMO_CODES: Record<string, number> = { |
| 11 | # LAUNCH40: 0.4, |
| 12 | # PARTNER_FRIENDS: 0.5, |
| 13 | # }; |
| 14 | # |
| 15 | # export function quote(seats: number, annual: boolean, promo?: string) { |
Це мій TypeScript, з типами й іменами констант, прямо з файлу на моєму диску. DevTools робить те саме мовчки: щойно мапа завантажилася, вкладка Sources показує ваше оригінальне дерево папок поруч із бандлом. Саме опублікована мапа зробила з npm-пакета Claude Code новину, хоча його бандл і до того весь час був читабельним.
Чи може AI прочитати мініфікований або обфускований JavaScript?
Так. Сучасна модель пояснює мініфікований бандл з одного промпту, аж до формули ціни та відкритого адмінського хука, а AI-агенти читають і обфускований код: в одному з попередніх тестів агент повернув опубліковані демо двох обфускаторів до чистого вихідного коду за 10 і 20 хвилин.
27.09.2026 я дав мініфікований демо-бандл вище (без мапи) Claude Sonnet 5 через Claude Code 2.1.283, без інструментів, із таким промптом, а після нього — бандл у блоці коду js:
| This is a minified JavaScript file I downloaded from a website. Explain what it does in plain English: the pricing rules, the feature flags, the API endpoints it calls, and anything a competitor could copy or abuse. Be concise. |
Модель відновила повну формулу ціни, аж до суми, яку платить річний план на 10 місць із LAUNCH40: $57.60. А ще знайшла те, що я підклав і на що не вказував:
window.__debug.grantTrial/fetchInvoicesвиставляють функції адмінського продовження триалу та отримання рахунків прямо в глобальну область видимості продакшн-JS. Будь-хто може відкрити devtools і викликати__debug.grantTrial('<any user id>')з консолі браузера.
Повна відповідь, без редагування
| 1 | ## Pricing logic (`i()`) |
| 2 | - Base rate: **$12/seat/month** (`e*1200` in cents). |
| 3 | - **25+ seats** → extra 10% off (stacks). |
| 4 | - Annual billing (`t` truthy) → 20% off. |
| 5 | - Promo codes (case-insensitive): `LAUNCH40` = 40% off, `PARTNER_FRIENDS` = 50% off. Discounts stack multiplicatively (seat discount × annual × promo). |
| 6 | - Example in the code: 10 seats, annual, `launch40` → $120 → $96 (annual) → $57.60 (promo), shown as `57.6` on `#price`. |
| 7 | |
| 8 | ## Feature gating |
| 9 | - Export limit: free plan capped at **<3 exports/month**; every paid plan is unlimited (`a()`). |
| 10 | - Flags: `newCheckout: true`, `aiSummaries: false`, `teamSeatsBeta: true` (`l`). |
| 11 | - `aiSummaries` is force-enabled for `pro`/`team` plans regardless of the global flag — so it's effectively live for paying users even though the flag reads "off" (likely a soft-launch/A-B mechanism). |
| 12 | |
| 13 | ## API endpoints |
| 14 | - `GET /v2/billing/invoices` — bearer-token authenticated, returns invoice list. |
| 15 | - `POST /internal/admin/trials/{userId}/extend` — an **internal/admin** endpoint shipped in a public client bundle, with no auth header attached in this call at all. |
| 16 | |
| 17 | ## What a competitor/attacker could copy or abuse |
| 18 | - **Hardcoded discount codes** (`LAUNCH40`, `PARTNER_FRIENDS`) are fully visible client-side, including their exact percentages — trivial to discover and use even if not publicly advertised. |
| 19 | - **Exact free-tier limit** (3 exports/month) and pricing formula are exposed, handy for competitive benchmarking/undercutting. |
| 20 | - **`window.__debug.grantTrial` / `fetchInvoices`** expose the admin trial-extension and invoice-fetch functions directly in the global scope of production JS. Anyone can open devtools and call `__debug.grantTrial('<any user id>')` from the browser console. If the server doesn't independently re-verify admin privileges (and the client sends no auth token here), this is a **self-service trial/privilege-extension exploit** — the single biggest risk in this file. |
| 21 | - Naming leaks internal API structure (`/internal/admin/...`), which helps an attacker guess/probe other admin routes. |
| 22 | - `teamSeatsBeta` flag reveals an unreleased feature (competitive roadmap intel). |
| 23 | |
| 24 | **Bottom line:** the real vulnerability isn't the pricing math, it's that debug/admin hooks (`grantTrial`) were left reachable from `window` in production — that should be stripped from the shipped bundle and the endpoint should require server-side admin auth regardless. |
Я звірив це з вихідним кодом: кожен факт правильний. Єдине неточне формулювання — <3 exports/month, але далі модель сама правильно називає ліміт у 3. На все пішов один промпт, і надіслати його може будь-хто, кому цікавий ваш продукт.
Як просканувати сайт на відкритий вихідний код і source maps
Вставте URL свого продакшн-сайту в безкоштовний сканер безпеки AfterPack; акаунт не потрібен. Сканер рендерить сторінку в справжньому браузері, збирає кожен скрипт, який вона завантажує, і показує відкриті секрети, публічні source maps і те, наскільки захищений кожен бандл. Той самий сканер запускається з термінала:
| npx afterpack@latest audit https://example.com |
Аудит живого сайту перелічує, що саме перевіряється, і коди виходу для CI.
Як захистити вихідний код JavaScript
Спершу приберіть те, чого обфускація не захистить, а тоді обфускуйте решту: публічний source map або ключ у бандлі видають код, хоч як би був записаний сам бандл.
| Крок | Від чого захищає | Як |
|---|---|---|
| 1. Не віддавати source maps | Від публікації оригінальних файлів, типів і коментарів | Приховані мапи, завантажені приватно; .map-файли видалені з деплою |
| 2. Перенести секрети й повноваження на сервер | Від читання ключів, обходу лімітів і адмінських маршрутів | Перевірки на сервері; перевірка в клієнті — лише для UI |
| 3. Обфускувати логіку, яка мусить потрапити в браузер | Від пошуку рядків, читання й патчингу логіки | npx afterpack@latest dist/ --preset=hard або плагін для фреймворка |
Як вимкнути source maps у продакшні
Не віддавайте їх. Якщо вони потрібні для трекінгу помилок, генеруйте їх без коментаря sourceMappingURL:
| Бандлер або фреймворк | Налаштування | Що ви отримуєте |
|---|---|---|
| esbuild | --sourcemap=external | .map-файл, без коментаря в бандлі |
| Vite | build.sourcemap: "hidden" | .map-файл, без коментаря в бандлі |
| webpack | devtool: "hidden-source-map" | .map-файл, без коментаря в бандлі |
| Rollup | output.sourcemap: "hidden" | .map-файл, без коментаря в бандлі |
| Next.js | productionBrowserSourceMaps | Вимкнено за замовчуванням; перевірте, що ніхто не ввімкнув |
| Create React App | GENERATE_SOURCEMAP=false | Жодних мап (за замовчуванням вони ввімкнені) |
Завантажте мапи приватно у свій трекер помилок, а тоді видаліть .map-файли з директорії деплою. Прихована мапа, що лишилася на сервері, все одно на відстані однієї вгаданої URL-адреси. У розділі Налагодження продакшн-трейсів без публікації мапи є правило на edge для випадку, коли одна все ж проскочила.
Тримайте секрети й перевірки на сервері
API-ключ у бандлі може прочитати будь-хто, хто запускає сторінку, хоч що ви робите з файлом. Те саме з рішеннями: ліміт безкоштовного тарифу, перевірений лише в браузері, — це побажання, промокодам місце в серверному довіднику, а адмінський маршрут у моєму демо захищений рівно настільки, наскільки власна перевірка сервера. Перевірку в клієнті лишайте для UI. Забезпечуйте її на сервері.
Обфускуйте JavaScript, який мусить потрапити в браузер
Частина коду працює лише в браузері, як-от живий калькулятор ціни чи редактор у браузері. Для такого коду запустіть AfterPack на виводі збірки:
| npx afterpack@latest dist/ --preset=hard |
Без --preset ви отримаєте light, який кодує рядкові літерали. Це зупиняє текстовий пошук ваших маршрутів і прапорців, але не робить математику цін важкою для розуміння. Для коду, який захотів би конкурент, як-от ціноутворення й обмеження за тарифом, використовуйте hard або спершу вставте функцію в playground і подивіться, що з нею робить кожен рівень. У пресетах перелічено, що додає кожен рівень. Після hard моє демо так само рахує $57.60 (AfterPack 0.2.0, 29.09.2026), а якщо хочете, щоб це робила ваша звичайна збірка, є плагіни для фреймворків.
Форма важить не менше за пресет. Незмінний бандл — це мішень: модель чи скрипт один раз роблять його реверс-інжиніринг, і результат працює до вашого наступного релізу. AfterPack бере новий сід на кожну збірку, тож кожен реліз — структурно інша програма, і скрипт деобфускації чи анотована мапа, зроблені під бандл минулого тижня, до цього тижня вже не підходять. З @afterpack/wasm у вашому власному Cloudflare Worker код може змінювати форму на кожен запит; у туторіалі про Workers є повне налаштування, а пост про запуск показує одну функцію до й після.
Почніть з одного пошуку: відкрийте DevTools на своєму продакшн-сайті й пошукайте internal. Усе, що знайдеться, вже є в кожного відвідувача.
Часті запитання
Чи можна повністю сховати JavaScript від користувачів?
Ні. Щоб виконати код, браузер мусить його завантажити, тож будь-хто може прочитати те, що отримав. Ви можете тримати секрети й повноваження на сервері, перестати публікувати source maps і зробити код, що їде в браузер, дорогим для читання за допомогою обфускації.
Як не дати скопіювати чи зробити реверс-інжиніринг мого JavaScript?
Завантаженню не завадиш; але копію можна зробити дорогою для читання й такою, що швидко застаріває. Обфускуйте з --preset=hard, відвантажуйте нову форму з кожною збіркою (або з кожним запитом зі свого Worker) і тримайте повноваження на сервері, щоб скопійований клієнт не міг більше, ніж оригінал. Anti-tamper у Pro робить так, що змінена копія ламається, а не працює.
Чи ховає обфускація API-ключі?
Ні. Ключ у бандлі може прочитати будь-хто, хто запускає сторінку, обфускований він чи ні. Тримайте ключі на сервері й викликайте API звідти; модель загроз пояснює, від чого обфускація захищає, а від чого ні.
Чи працює обфускований JavaScript так само?
Має працювати, а коли не може, ви дізнаєтеся про це під час збірки: файл, який рушій не може обфускувати, валить збірку й називає цей файл, тож зламаний бандл ніколи не відвантажиться непомітно. Запускайте інтеграційні тести на обфускованій збірці, а не на звичайній: відвантажується саме вона.
AfterPack безкоштовний?
Локальний рушій безкоштовний з усіма пресетами, а CLI і плагіни для фреймворків мають відкритий код. Pro додає директиви для окремих ділянок коду, anti-tamper і хмарні збірки з оплатою за мегабайт; цифри є на сторінці тарифів.
— Nikita
