Перейти к основному содержимому

ADR 0117 — website/ получает собственный lockfile, вне pnpm audit продукта

  • Статус: принят
  • Дата: 2026-08-28

Контекст​

website/ (сайт Docusaurus, публикующий docs/) является участником корневого pnpm-workspace.yaml с момента своего создания, разделяя единственный pnpm-lock.yaml с apps/api, apps/web, apps/runner, packages/* и scripts. Ничто в website/ не попадает в production-образ — сайт собирается и публикуется собственным workflow (docs-deploy.yml/docs-check.yml) и никогда не упаковывается в артефакты api, web или runner.

Общий lockfile взимал две цены, не имевшие ничего общего с продуктом:

  1. pnpm audit перестал отвечать на вопрос «безопасен ли продукт?» и стал отвечать на «безопасны ли продукт и генератор документации?». Docusaurus, docusaurus-theme-openapi-docs и их транзитивное дерево (webpack, postman-collection, oas-validator, @redocly/*…) — это большая и быстро меняющаяся поверхность, которая никогда не поставляется, и каждый advisory в ней появлялся на том же шлюзе pnpm audit, от которого зависит продукт (ci.yml, job «Auditoria de dependências»).
  2. Большинство overrides безопасности в pnpm-workspace.yaml существовало только для исправления транзитивной зависимости сайта. Из 13 записей, проаудированных для этого решения, serialize-javascript и диапазон yaml@1.x использовались исключительно деревом зависимостей website/ (подтверждено с помощью pnpm why <pkg> -r по общему lockfile, а не перечитыванием комментариев — см. «Что нашёл аудит» ниже). Держать их в корне означало нести поверхность уязвимостей website/ как будто она принадлежит продукту.

Что нашёл аудит​

Запись backlog, предложившая это, предполагала, что разделение будет чистым: «overrides website уходят, overrides продукта остаются». Запуск pnpm why <pkg> -r по lockfile до разделения для 13 overrides показал более запутанную реальность: несколько пакетов, которые комментарии приписывали только website/, также подтягиваются apps/api или apps/web по цепочке, отличной от описанной в комментарии:

пакетцепочка самого websiteтакже требуется
js-yaml (<4.3.1)openapi-to-postmanv2 / @11ty/gray-matterapps/api, через eslint → @eslint/eslintrc
js-yaml (<5.2.2)@redocly/openapi-coreapps/api, через @nestjs/swagger (уже известно)
mermaid@docusaurus/theme-mermaidapps/web, прямая runtime-зависимость (ADR 0068)
dompurifyчерез mermaidapps/web, через тот же mermaid
uuidpostman-collection, sockjsapps/web, через mermaid
postcsspostcss-preset-env (транзитивная от docusaurus-theme-openapi-docs)apps/web, через собственную зависимость vite, postcss: ^8.5.17
nanoidчерез собственную цепочку postcssapps/web, через ту же цепочку postcss под vite
fast-uri@redocly/ajvapps/api, через ajv → @angular-devkit/core → @nestjs/schematics
lodash@docusaurus/core, прямаяapps/api, через @nestjs/swagger (уже известно)
undicicheerio (локальный поиск)apps/web, devDependency jsdom (уже известно — единственный случай, который запись backlog пометила как смешанный)

Лишь два override оказались действительно исключительными для website (serialize-javascript, yaml@1.x), и один оказался исключительным для продукта, без какого-либо присутствия в дереве website/ (esbuild, через drizzle-kit).

Это важно для решения ниже: разделение lockfile не означает, что большинство overrides просто «переезжают». Ранее общий override, исправляющий пакет, присутствующий с обеих сторон, должен продолжать защищать оба дерева, когда они перестают разделять одно разрешение, — убрать его с одной стороны, потому что комментарий называл другую, тихо вновь открыло бы CVE на той стороне, которая его потеряла.

Решение​

1. website уходит из списка packages: в pnpm-workspace.yaml​

Он перестаёт быть участником workspace. pnpm install в корне репозитория перестаёт трогать что-либо под website/, а apps/api/apps/web/apps/runner/packages/*/scripts продолжают устанавливаться ровно как раньше.

2. website/ получает собственный pnpm-lock.yaml, устанавливаемый независимо​

pnpm install, запущенный внутри website/ (не pnpm --filter website из корня — этот глагол требует быть участником workspace), разрешает и фиксирует зависимости website/ самостоятельно, только по website/package.json.

3. Overrides разделены по тому, где уязвимый пакет действительно разрешается, а не по тому, какой комментарий его называл​

  • Исключительные для website (serialize-javascript, yaml@1.x): удалены из корневого pnpm-workspace.yaml и добавлены в новый блок pnpm.overrides в website/package.json (синтаксис отдельного проекта — внутри website/ нет pnpm-workspace.yaml, поэтому ключ overrides: уровня workspace там не действует).
  • Смешанные (js-yaml в обоих диапазонах, mermaid, dompurify, uuid, postcss, nanoid, fast-uri, lodash, undici): сохранены в корне и продублированы в website/package.json. Два lockfile теперь разрешают эти пакеты независимо, поэтому каждому нужен собственный нижний предел.
  • Исключительные для продукта (esbuild): остаются только в корне. website/ никогда его не разрешает, поэтому запись там была бы мёртвым грузом.

Комментарии в обоих файлах говорят, в какую категорию попадает каждая запись, и указывают на этот ADR, чтобы следующему человеку, который тронет одну из смешанных записей, не пришлось запускать pnpm why с нуля, чтобы узнать, что другой стороне нужно то же исправление.

4. Скрипты и workflows переходят с --filter website на --dir website​

pnpm --filter <name> выбирает пакет по принадлежности к workspace — когда website вне workspace, каждый скрипт docs:* в корневом package.json (start, build, serve, clear) переходит на pnpm --dir website <script>, который запускает pnpm так, как если бы он был запущен внутри этого каталога, участника или нет. Имена скриптов, которые люди уже набирают (pnpm docs:build, pnpm docs:start), не меняются.

docs-deploy.yml и docs-check.yml получают каждый второй отдельный шаг pnpm install --frozen-lockfile, ограниченный website/ (working-directory: website), сразу после уже существующей установки в корне. Установка в корне больше не достигает зависимостей website/, и без этого второго шага шаг сборки упал бы из-за отсутствия node_modules.

Последствия​

  • pnpm audit в корне теперь сообщает только о дереве зависимостей продукта. Docusaurus, его тема, плагин OpenAPI и их транзитивные пакеты исчезают со шлюза, который запускает job «Auditoria de dependências» в ci.yml, — поверхность CVE, о которой он сообщает, сужается до того, что действительно поставляется.
  • Удобство единой установки закончилось. Изменение, затрагивающее пакет продукта и website/ (редкое, но вывод docs:generate затрагивает оба), теперь требует двух запусков pnpm install, одного в корне и одного внутри website/, вместо одного. docs-deploy.yml/docs-check.yml платят ту же цену вторым шагом CI.
  • У смешанного override два дома, которые нужно держать синхронными. Если apps/web откажется от mermaid (или vite сам поднимет postcss за пределы уязвимого диапазона), корневую запись можно будет удалить обычным образом (pnpm audit перестанет жаловаться), но копия в website/package.json этого не заметит, пока кто-нибудь не запустит pnpm audit внутри website/ тоже — единой команды, которая проверяла бы оба дерева сразу, пока нет.
  • Правило site-e-publicacao в docs/.docmap.yml уже наблюдает за website/** и .github/workflows/docs-deploy.yml в warn, поэтому будущее изменение в любом из них по-прежнему требует взглянуть на docs/explanation/documentation-workflow.md, который теперь также фиксирует схему с двумя lockfile.

Отвергнутые альтернативы​

  • Оставить website/ в workspace и просто терпеть его CVE в pnpm audit: status quo. Отвергнуто, потому что весь смысл шлюза аудита — предупреждать команду, когда у поставляемого пакета есть известная уязвимость, а пакет темы Docusaurus топит этот сигнал в шуме, с которым никто ничего не может сделать, кроме как поднять зависимость инструмента документации.
  • Перенести website/ в отдельный репозиторий: решает ту же проблему и больше, но теряет источник документации (docs/ остаётся в этом репозитории), живущий рядом с сайтом, который его отображает, а pipeline docs:generate, читающему код apps/api напрямую для сборки справочника OpenAPI, понадобился бы шаг между репозиториями. Вне рамок того, что просила запись backlog.
  • Держать все overrides в корне «на всякий случай»: проще написать и неправильно точно так же, как были неправильны исходные комментарии со смешанной атрибуцией, — скрывает, какой пакет действительно нуждается в защите на какой стороне, и следующее разделение (или следующий аудит этих записей) снова начинается с нуля.