Зачіпка
npm одночасно підсилив дві болючі ділянки JavaScript supply chain: фінальний publish пакета і джерела, з яких проєкт дозволяє встановлювати залежності.
Staged publishing став загальнодоступним, а npm CLI 11.15.0+ отримав нові allow-* controls для file, remote і directory sources. Разом із allow-git це вже не просто новина про npm, а нормальний привід переглянути release pipeline.
Проблема / Контекст
Багато команд роками живуть із простим flow: CI збирає пакет, запускає тести і робить npm publish. Це зручно, поки все працює. Але якщо CI token занадто широкий, workflow скомпрометований або release job запускається не там, де треба, пакет одразу вилітає в registry.
Staged publishing додає паузу між автоматичною підготовкою пакета і фінальною публікацією. CI може виконати npm stage publish, але останній крок робить людина через CLI або npmjs.com. Для сильнішої моделі npm прямо поєднує це з trusted publishing, restricted token access і stage-only permissions.
Друга частина змін про install. Залежності не завжди приходять із registry. У package.json можуть бути git URL, локальні файли, директорії або remote tarballs. Іноді це легальний workflow. Іноді це випадковий обхід нормального dependency review.
Нові allow-file, allow-remote, allow-directory доповнюють allow-git. Кожне правило може мати значення all, none або root, тож політика не зводиться до “зламати все” або “дозволити все”.
Чому це важливо
Publish і install - це дві сторони одного ризику. Publish визначає, що команда випускає назовні. Install визначає, що команда тягне всередину.
Якщо CI може напряму publish-ити пакет, один зламаний workflow може стати production-інцидентом для всіх користувачів пакета. Якщо install безконтрольно дозволяє non-registry sources, у проєкт може потрапити залежність, яку складніше перевірити, відтворити й оновити.
Для бібліотек це ризик репутації. Для application repo це ризик непомітної залежності, яка обходить звичний registry review. Для platform-команди це ще й проблема повторюваності: build має бути зрозумілим через місяць, а не залежати від випадкового URL або локальної директорії.
Як це зробити
1. Знайдіть прямий publish
Почніть із пошуку release jobs:
rg "npm publish|npm stage publish|NPM_TOKEN|NODE_AUTH_TOKEN" .github .gitlab-ci.yml package.json .npmrc
Якщо бачите npm publish у CI, поставте просте питання: цей job має право одразу змінити public registry чи лише підготувати release?
2. Оновіть runtime для stage flow
Staged publishing потребує npm CLI 11.15.0+ і Node.js 22.14.0+. Зафіксуйте це в CI, а не покладайтеся на image “latest”:
- uses: actions/setup-node@v4
with:
node-version: 22.14.0
- run: npm -v
Після цього замініть фінальний publish у CI на:
npm stage publish --provenance
Фінальний approve лишіть людині з 2FA. Ідея не в тому, щоб зробити release повільним. Ідея в тому, щоб CI не був останньою інстанцією перед registry.
3. Звузьте права CI
Найкращий варіант - trusted publishing без довгоживучого npm token. Якщо token ще потрібен, він не має бути універсальним publish-ключем на все.
Для trust relationship перевірте, що CI має stage-only permission. Тоді навіть скомпрометований job не зможе одразу опублікувати пакет, а лише створить staged version, яку треба окремо підтвердити.
4. Перевірте non-registry dependencies
Знайдіть залежності не з registry:
node -e "const p=require('./package.json'); for (const [k,v] of Object.entries({...p.dependencies,...p.devDependencies})) if (/^(git\\+|https?:|file:|\\.\\.?\\/)/.test(v)) console.log(k, v)"
Потім почніть з помірної політики:
npm install --allow-git=root --allow-remote=none --allow-file=root --allow-directory=root
root означає: дозволити тільки те, що явно описано у root package. Це хороший проміжний режим для команд, які мають свідомі локальні workspace-патерни, але не хочуть неочікуваних джерел у transitive dependency tree.
5. Додайте CI smoke test
Окремий job має перевіряти install policy:
npm ci --allow-git=root --allow-remote=none --allow-file=root --allow-directory=root
Якщо він падає, не вимикайте політику одразу. Спершу з’ясуйте, яке джерело блокується і чи воно справді потрібне.
Антипатерни
- лишити
npm publishу CI з broad token і назвати це automation; - увімкнути staged publishing, але дати CI повний publish access;
- approve-ити staged release без перегляду package contents;
- дозволити
allow-remote=all, бо один старий пакет так працює; - не фіксувати Node.js і npm CLI у release job;
- змішати publish hardening з великим refactor release pipeline.
Висновок / План дій
Мінімальний здоровий rollout такий:
- знайти всі місця з
npm publish; - оновити release job до Node.js 22.14.0+ і npm CLI 11.15.0+;
- перевести CI на
npm stage publish; - залишити фінальний approve за людиною з 2FA;
- звузити CI до trusted publishing або stage-only permission;
- перевірити package.json на non-registry sources;
- додати
npm ciз allow-* policy; - задокументувати винятки й rollback.
Офіційні джерела:
Короткий чеклист
- Перевірити, чи CI має пряме право npm publish.
- Оновити Node.js і npm CLI до версій, які підтримують staged publishing.
- Замінити прямий publish на npm stage publish.
- Залишити фінальний approve за людиною з 2FA.
- Обмежити non-registry sources через allow-* controls.
- Задокументувати rollback для release pipeline.
Prompt Pack: перевірити npm publish та install policy
Допоможи перевірити npm supply-chain hardening у нашому Node.js проєкті. Вхідні дані: - package.json і .npmrc; - як зараз запускається npm publish у CI/CD; - чи використовується trusted publishing; - які npm tokens або trust relationships мають publish-права; - фрагмент package-lock.json або список non-registry dependencies; - поточна версія Node.js і npm CLI; - чи є ручний release approval. Поверни: 1. verdict: publish-flow безпечний, ризикований або критичний; 2. де є прямий publish із CI; 3. план переходу на npm stage publish і manual approve; 4. рекомендовані allow-git, allow-remote, allow-file, allow-directory; 5. CI smoke test для install policy; 6. rollback-план, якщо release або install зламається. Формат відповіді: verdict, ризики, diff команд, rollout на 30 хвилин, rollback.