Трендовые github проекты в нашем телеграм канале. Подпишись → Папка dist заслуживает отдельной ревизии перед релизом
Тесты зелёные, линтер молчит, pull request одобрен. Кажется, что код проверен со всех сторон. Но ревью читает исходники, а в браузер пользователя уезжает собранный артефакт: минифицированные чанки, карты исходников, случайно попавшие JSON-конфиги. Между тем, что написано в репозитории, и тем, что лежит в dist, помещается целый класс утечек, которые не видно ни в diff, ни в .env.production.
Разберём воспроизводимую процедуру: собрать контрольный релиз, провести инвентаризацию файлов, прогнать поиск по подозрительным паттернам и — самое важное — научиться отличать ожидаемое содержимое от инцидента.
Три разных представления одного релиза
Полезно держать в голове, что у любого фронтенд-релиза есть три независимых слоя:
- Исходный код — то, что разработчик планировал отправить пользователю.
- Production-артефакт — то, что реально сгенерировал сборщик после подстановки переменных и минификации.
- Процесс развёртывания — то, что из артефакта в итоге окажется на боевом домене.
Ошибки прячутся на стыках. Переменная окружения, которую забыли пометить как клиентскую; ветка кода, которую оптимизатор должен был вырезать; карта исходников, которую CI выложил рядом с бандлом. Всё это проходит ревью исходников без единого замечания.
Контрольный релиз с маркерами
Чтобы проверить процедуру, соберём заведомо неидеальный релиз. В .env.production кладём смесь легитимных клиентских значений и того, чему в браузере делать нечего:
VITE_PUBLIC_API_URL=https://api.production.example.test/v1
VITE_PUBLISHABLE_KEY=pk_test_MARKER_ONLY
VITE_SECRET_KEY=sk_test_MARKER_ONLY
VITE_STAGING_API_URL=https://staging-api.example.test/v1
VITE_EXPERIMENTAL_CHECKOUT=true
Чтобы значения действительно попали в сборку, приложение должно к ним обратиться. В src/main.js:
const clientConfig = {
apiUrl: import.meta.env.VITE_PUBLIC_API_URL,
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
secretKeyMistake: import.meta.env.VITE_SECRET_KEY,
stagingApiUrl: import.meta.env.VITE_STAGING_API_URL,
experimentalCheckout: import.meta.env.VITE_EXPERIMENTAL_CHECKOUT,
diagnosticLabel: "DEBUG_RETAINED_FALLBACK_TO_V1",
};
if (import.meta.env.DEV) {
console.debug("DEBUG_DEV_ONLY_SHOULD_BE_STRIPPED");
}
document.querySelector("#inventory").textContent =
JSON.stringify(clientConfig, null, 2);
Сборщики уровня Vite подставляют import.meta.env во время билда. Любая переменная с префиксом VITE_, к которой обращается код, становится частью клиентского бандла в открытом виде. Ветка под import.meta.env.DEV в production-режиме считается мёртвым кодом и должна быть удалена из исполняемого файла.
Инвентаризация вместо слепого grep
Первый шаг — не поиск секретов, а список того, из чего вообще состоит релиз:
find dist -type f | sort
Так обнаруживаются забытые *.map, отчёты бандл-анализатора, тестовые фикстуры и конфиги, которые не читаются при просмотре исходников. Только после этого имеет смысл искать по содержимому:
rg -n 'secret|token|api|staging|debug|sourceMappingURL' dist -g '*.js'
Вывод такой команды — сырой материал, а не готовый отчёт об инциденте. Слово api в терминале означает лишь то, что находку нужно открыть, понять её назначение и решить, место ли ей в публичной сборке.
Разбор находок
Одинаковая отметка «есть в бандле» скрывает совершенно разные ситуации.
Production-адрес API. Браузеру нужно знать, куда слать запросы, поэтому URL в клиентском конфиге ожидаем. Проверяем не отсутствие адресов, а соответствие содержимого ожиданиям: тот ли это домен, та ли версия API.
Два ключа с похожими именами. pk_test_… — публикуемый ключ, он рассчитан на работу в браузере и не даёт доступа к операциям аккаунта; его присутствие в JS нормально. sk_test_… — серверный ключ, он подтверждает привилегированные запросы к платёжному провайдеру. Даже тестовый серверный ключ в клиентской сборке — повод остановить развёртывание и ротировать секрет.
Staging-URL. Сам по себе адрес тестового стенда секретом не является, но мы проверяем production-артефакт, а внутри лежит конфигурация другого окружения. Причина обычно в одном из трёх: конвейер подтянул не тот .env, адрес забыли вычистить после отладки, либо он действительно нужен коду и это надо зафиксировать явно.
Фича-флаг. VITE_EXPERIMENTAL_CHECKOUT=true попадает в бандл целиком, вместе со всей логикой ветки. Клиентский флаг годится для управления интерфейсом. Если за флагом скрыт доступ к платным или приватным операциям, проверку обязательно дублировать на бэкенде: пользователь легко переключит значение через DevTools или дёрнет закрытый метод напрямую.
Debug-строка в исполняемом коде. Маркер DEBUG_RETAINED_FALLBACK_TO_V1 доступов не раскрывает, но показывает механику: служебный текст остаётся в бандле, если используется в живом коде. В крупном проекте такие строки выдают внутренние режимы, старые фоллбеки и детали незавершённых экспериментов. Строку для мониторинга оставляем осознанно, забытый след отладки убираем.
Source map хранит то, что минификатор выбросил
Вторая debug-строка, DEBUG_DEV_ONLY_SHOULD_BE_STRIPPED, из .js действительно исчезла — оптимизатор вырезал мёртвую ветку. Но проверять только .js недостаточно:
rg -n 'DEBUG_DEV_ONLY_SHOULD_BE_STRIPPED|sourcesContent' dist -g '*.map'
В карте исходников строка на месте, в секции sourcesContent лежит исходный текст модуля целиком. Это ожидаемое поведение: задача минификатора — убрать лишнее из исполняемого файла, задача source map — сохранить оригинал для отладки. Проблема возникает, когда .map публикуется вместе с бандлом и становится доступен любому по прямой ссылке из sourceMappingURL.
Варианты решения: не генерировать карты для production, генерировать «скрытые» карты без комментария-ссылки в конце файла, либо выкладывать .map только во внутренний сервис ошибок и не пускать их на CDN.
Что вынести в CI
Ручная ревизия полезна как разовое упражнение, дальше её стоит автоматизировать:
- шаг пайплайна, который падает при появлении в
distфайлов*.map(или, наоборот, проверяет, что они загружены только в трекер ошибок); - поиск по бандлу известных префиксов секретов —
sk_,AKIA,-----BEGIN,ghp_— с ненулевым кодом возврата; - проверка, что в сборку не попали адреса непроизводственных окружений;
- фиксация списка файлов релиза в артефактах сборки, чтобы неожиданные добавления были видны в diff между релизами.
Короткий чек-лист
- Сначала список файлов
dist, потом поиск по содержимому. - Для каждой находки — решение «ожидаемо / убрать / инцидент», а не просто «нашлось».
- Серверные ключи в клиентском коде — стоп-фактор и ротация.
- Клиентские фича-флаги всегда дублируются проверкой на бэкенде.
- Source map либо не публикуются, либо уходят только во внутренний сервис.
- Всё перечисленное держится проверками в пайплайне, а не дисциплиной разработчика.
Папка dist не должна оставаться чёрным ящиком. Заглядывать туда стоит до того, как релиз станет публичным.