# Як захистити вихідний код JavaScript у продакшні

Будь-хто може прочитати JavaScript вашого сайту; захистіть його в продакшні: приберіть source maps, тримайте секрети на сервері й обфускуйте через AfterPack.

Source: https://www.afterpack.dev/uk/blog/protect-javascript-source-code

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

Щоб захистити вихідний код JavaScript, перестаньте віддавати source maps, тримайте секрети й перевірки доступу на сервері та обфускуйте код, який мусить потрапити в браузер. Кожен відвідувач завантажує ваш бандл, а мініфікація лише скорочує локальні імена: рядки, API-маршрути, feature flags і правила ціноутворення лишаються читабельними. AfterPack (afterpack.dev), обфускатор JavaScript, перетворює ваш бандл на структурно іншу програму з кожною збіркою. Його CLI з відкритим кодом працює безкоштовно й локально через [`npx afterpack`](https://www.afterpack.dev/docs/quickstart); Pro додає директиви для окремих ділянок коду та anti-tamper.

Щоб показати, як це виглядає, я зібрав невеликий демо-застосунок: чотири TypeScript-файли з ціною за місце, промокодами, feature flags, лімітом експорту на безкоштовному тарифі та двома викликами API. Забандлив його esbuild 0.28.1 (`--bundle --minify --sourcemap`) 27.09.2026. Усе нижче — реальний вивід цієї збірки, а ті самі перевірки на власному продакшн-сайті займуть близько п'яти хвилин: DevTools, один `curl` і безкоштовний [сканер безпеки](https://www.afterpack.dev/security-scanner).

## Чи можуть користувачі побачити ваш 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 байтів:

```js
(()=>{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:

```bash
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](https://tc39.es/ecma426/): ваші оригінальні файли, рівно такими, як ви їх написали. Два виклики `jq` перелічують їх і друкують один:

```bash
curl -s https://example.com/assets/app.js.map | jq -r '.sources[]'
# ../src/pricing.ts
# ../src/flags.ts
# ../src/api.ts
# ../src/app.ts

curl -s https://example.com/assets/app.js.map | jq -r '.sourcesContent[0]' | head -8
# const SEAT_PRICE_CENTS = 1200;
# const ANNUAL_DISCOUNT = 0.2;
# const PROMO_CODES: Record<string, number> = {
#   LAUNCH40: 0.4,
#   PARTNER_FRIENDS: 0.5,
# };
#
# export function quote(seats: number, annual: boolean, promo?: string) {
```

Це мій TypeScript, з типами й іменами констант, прямо з файлу на моєму диску. DevTools робить те саме мовчки: щойно мапа завантажилася, вкладка Sources показує ваше оригінальне дерево папок поруч із бандлом. Саме опублікована мапа зробила з [npm-пакета Claude Code](https://www.afterpack.dev/blog/claude-code-source-leak) новину, хоча його бандл і до того весь час був читабельним.

## Чи може AI прочитати мініфікований або обфускований JavaScript?

Так. Сучасна модель пояснює мініфікований бандл з одного промпту, аж до формули ціни та відкритого адмінського хука, а AI-агенти читають і обфускований код: в [одному з попередніх тестів](https://www.afterpack.dev/blog/ai-deobfuscates-javascript) агент повернув опубліковані демо двох обфускаторів до чистого вихідного коду за 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>')` з консолі браузера.

**Повна відповідь, без редагування**

```
## Pricing logic (`i()`)
- Base rate: **$12/seat/month** (`e*1200` in cents).
- **25+ seats** → extra 10% off (stacks).
- Annual billing (`t` truthy) → 20% off.
- Promo codes (case-insensitive): `LAUNCH40` = 40% off, `PARTNER_FRIENDS` = 50% off. Discounts stack multiplicatively (seat discount × annual × promo).
- Example in the code: 10 seats, annual, `launch40` → $120 → $96 (annual) → $57.60 (promo), shown as `57.6` on `#price`.

## Feature gating
- Export limit: free plan capped at **<3 exports/month**; every paid plan is unlimited (`a()`).
- Flags: `newCheckout: true`, `aiSummaries: false`, `teamSeatsBeta: true` (`l`).
- `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).

## API endpoints
- `GET /v2/billing/invoices` — bearer-token authenticated, returns invoice list.
- `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.

## What a competitor/attacker could copy or abuse
- **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.
- **Exact free-tier limit** (3 exports/month) and pricing formula are exposed, handy for competitive benchmarking/undercutting.
- **`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.
- Naming leaks internal API structure (`/internal/admin/...`), which helps an attacker guess/probe other admin routes.
- `teamSeatsBeta` flag reveals an unreleased feature (competitive roadmap intel).

**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 свого продакшн-сайту в безкоштовний [сканер безпеки](https://www.afterpack.dev/security-scanner) AfterPack; акаунт не потрібен. Сканер рендерить сторінку в справжньому браузері, збирає кожен скрипт, який вона завантажує, і показує відкриті секрети, публічні source maps і те, наскільки захищений кожен бандл. Той самий сканер запускається з термінала:

```bash
npx afterpack@latest audit https://example.com
```

[Аудит живого сайту](https://www.afterpack.dev/docs/audit) перелічує, що саме перевіряється, і коди виходу для CI.

## Як захистити вихідний код JavaScript

Спершу приберіть те, чого обфускація не захистить, а тоді обфускуйте решту: публічний source map або ключ у бандлі видають код, хоч як би був записаний сам бандл.

| Крок | Від чого захищає | Як |
| --- | --- | --- |
| 1. Не віддавати source maps | Від публікації оригінальних файлів, типів і коментарів | Приховані мапи, завантажені приватно; `.map`-файли видалені з деплою |
| 2. Перенести секрети й повноваження на сервер | Від читання ключів, обходу лімітів і адмінських маршрутів | Перевірки на сервері; перевірка в клієнті — лише для UI |
| 3. Обфускувати логіку, яка мусить потрапити в браузер | Від пошуку рядків, читання й патчингу логіки | `npx afterpack@latest dist/ --preset=hard` або плагін для фреймворка |

> **Оновлення, жовтень 2026.** `hard` на весь бандл у рази збільшує час запуску сторінки ([ціна завантаження](https://www.afterpack.dev/docs/presets#load-cost)). Для всього бандла застосунку правильний пресет — `light`; у [Pro](https://www.afterpack.dev/docs/pro) [директиви](https://www.afterpack.dev/docs/directives) ставлять `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-адреси. У розділі [Налагодження продакшн-трейсів без публікації мапи](https://www.afterpack.dev/docs/best-practices#debug-production-traces-without-publishing-the-map) є правило на edge для випадку, коли одна все ж проскочила.

### Тримайте секрети й перевірки на сервері

API-ключ у бандлі може прочитати будь-хто, хто запускає сторінку, хоч що ви робите з файлом. Те саме з рішеннями: ліміт безкоштовного тарифу, перевірений лише в браузері, — це побажання, промокодам місце в серверному довіднику, а адмінський маршрут у моєму демо захищений рівно настільки, наскільки власна перевірка сервера. Перевірку в клієнті лишайте для UI. Забезпечуйте її на сервері.

### Обфускуйте JavaScript, який мусить потрапити в браузер

Частина коду працює лише в браузері, як-от живий калькулятор ціни чи редактор у браузері. Для такого коду запустіть AfterPack на виводі збірки:

```bash
npx afterpack@latest dist/ --preset=hard
```

Без `--preset` ви отримаєте `light`, який кодує рядкові літерали. Це зупиняє текстовий пошук ваших маршрутів і прапорців, але не робить математику цін важкою для розуміння. Для коду, який захотів би конкурент, як-от ціноутворення й обмеження за тарифом, використовуйте `hard` або спершу вставте функцію в [playground](https://www.afterpack.dev/playground) і подивіться, що з нею робить кожен рівень. У [пресетах](https://www.afterpack.dev/docs/presets) перелічено, що додає кожен рівень. Після `hard` моє демо так само рахує $57.60 (AfterPack 0.2.0, 29.09.2026), а якщо хочете, щоб це робила ваша звичайна збірка, є [плагіни для фреймворків](https://www.afterpack.dev/docs/frameworks).

Форма важить не менше за пресет. Незмінний бандл — це мішень: модель чи скрипт один раз роблять його реверс-інжиніринг, і результат працює до вашого наступного релізу. AfterPack бере новий сід на кожну збірку, тож кожен реліз — структурно інша програма, і скрипт деобфускації чи анотована мапа, зроблені під бандл минулого тижня, до цього тижня вже не підходять. З [`@afterpack/wasm`](https://www.afterpack.dev/docs/workers) у вашому власному Cloudflare Worker код може змінювати форму на кожен запит; у [туторіалі про Workers](https://www.afterpack.dev/blog/obfuscate-javascript-cloudflare-workers) є повне налаштування, а [пост про запуск](https://www.afterpack.dev/blog/announcing-afterpack-javascript-obfuscator) показує одну функцію до й після.

Почніть з одного пошуку: відкрийте DevTools на своєму продакшн-сайті й пошукайте `internal`. Усе, що знайдеться, вже є в кожного відвідувача.

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

### Чи можна повністю сховати JavaScript від користувачів?

Ні. Щоб виконати код, браузер мусить його завантажити, тож будь-хто може прочитати те, що отримав. Ви можете тримати секрети й повноваження на сервері, перестати публікувати source maps і зробити код, що їде в браузер, дорогим для читання за допомогою обфускації.

### Як не дати скопіювати чи зробити реверс-інжиніринг мого JavaScript?

Завантаженню не завадиш; але копію можна зробити дорогою для читання й такою, що швидко застаріває. Обфускуйте з `--preset=hard`, відвантажуйте нову форму з кожною збіркою (або з кожним запитом зі свого Worker) і тримайте повноваження на сервері, щоб скопійований клієнт не міг більше, ніж оригінал. [Anti-tamper](https://www.afterpack.dev/docs/tiers) у Pro робить так, що змінена копія ламається, а не працює.

### Чи ховає обфускація API-ключі?

Ні. Ключ у бандлі може прочитати будь-хто, хто запускає сторінку, обфускований він чи ні. Тримайте ключі на сервері й викликайте API звідти; [модель загроз](https://www.afterpack.dev/docs/threat-model) пояснює, від чого обфускація захищає, а від чого ні.

### Чи працює обфускований JavaScript так само?

Має працювати, а коли не може, ви дізнаєтеся про це під час збірки: файл, який рушій не може обфускувати, валить збірку й називає цей файл, тож зламаний бандл ніколи не відвантажиться непомітно. Запускайте інтеграційні тести на [обфускованій збірці](https://www.afterpack.dev/docs/best-practices#test-the-obfuscated-build), а не на звичайній: відвантажується саме вона.

### AfterPack безкоштовний?

Локальний рушій безкоштовний з усіма пресетами, а CLI і плагіни для фреймворків мають відкритий код. Pro додає директиви для окремих ділянок коду, anti-tamper і хмарні збірки з оплатою за мегабайт; цифри є на сторінці [тарифів](https://www.afterpack.dev/docs/tiers).

— [Nikita](https://nikitaeverywhere.com)
