Compare commits
3 Commits
7f998d5222
...
1fb7fc2e90
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fb7fc2e90 | |||
| eb21b282d8 | |||
| 15c273c153 |
77
.wiki/concepts/verdaccio-restore-packument-desync.md
Normal file
77
.wiki/concepts/verdaccio-restore-packument-desync.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: Verdaccio disaster-restore desync — tarball present, packument stale → EEXISTS 409
|
||||||
|
type: concept
|
||||||
|
tags: [verdaccio, npm, storage, restore, backup, 409, gotcha, postmortem]
|
||||||
|
sources: []
|
||||||
|
updated: 2026-06-11
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verdaccio disaster-restore desync — 409 при publish свежей версии
|
||||||
|
|
||||||
|
## Симптом
|
||||||
|
|
||||||
|
После disaster-restore verdaccio из бэкапа: `npm/yarn publish <pkg>@<newver>` падает с
|
||||||
|
**`409 "this package is already present"`**, хотя в реестре этой версии (по GET) нет —
|
||||||
|
`dist-tags.latest` показывает древнюю версию. На некоторых пакетах GET вообще `404`.
|
||||||
|
|
||||||
|
## Root cause: packument ↔ tarball рассинхрон
|
||||||
|
|
||||||
|
Restore вернул в `/storage/.../<pkg>/`:
|
||||||
|
- **все `.tgz` тарболлы** — до текущих версий включительно (`data-0.9.0.tgz` физически на диске);
|
||||||
|
- но **`package.json` (packument-индекс) — древний** (`versions{}` знает только 0.1.x/0.2.x,
|
||||||
|
`dist-tags.latest=0.2.0`), потому что метадата восстановилась из старого слоя бэкапа
|
||||||
|
либо была затёрта сорванным `unpublish`.
|
||||||
|
|
||||||
|
Verdaccio publish-flow:
|
||||||
|
1. merge новой версии в packument → **OK** (версии нет в индексе);
|
||||||
|
2. запись `.tgz` на диск → файл **уже существует** (с restore) → `EEXISTS` →
|
||||||
|
surface как **`409 this package is already present`**.
|
||||||
|
|
||||||
|
Лог-отпечаток:
|
||||||
|
```
|
||||||
|
PUT /@snollajs%2Fdata
|
||||||
|
http <-- 409 ... error: this package is already present
|
||||||
|
```
|
||||||
|
|
||||||
|
### Подвид: пустой packument после сорванного unpublish
|
||||||
|
|
||||||
|
`unpublish` пишет пустой packument-stub (161 байт, `versions:[]`, `_rev:""`), затем пытается
|
||||||
|
`rmdir` каталог → **`ENOTEMPTY`** (тарболлы на месте) → `500`. Итог: GET `404` (пустой индекс),
|
||||||
|
publish `409` (тарболл цел). Каталог «полупустой»: индекс мёртв, бинарники живы.
|
||||||
|
|
||||||
|
## Fix: хирургия, НЕ `rm -rf` каталога
|
||||||
|
|
||||||
|
`rm -rf /storage/.../<pkg>` снимет 409, но **уничтожит всю историю версий** (тарболлы —
|
||||||
|
locally-published, `_distfiles:{}` ⇒ единственная копия, см. [[verdaccio-prune-semantics]]).
|
||||||
|
|
||||||
|
Правильно — удалить **только коллизирующий целевой `.tgz`** под версию, которую republish'ат:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# backup сначала
|
||||||
|
sudo tar czf /tmp/verdaccio-rescue-$(date -u +%Y%m%dT%H%M%SZ).tar.gz <pkg-dirs>
|
||||||
|
# снять коллизию — ровно целевой тарболл, остальное не трогать
|
||||||
|
sudo rm /opt/stacks/verdaccio/storage/@scope/<pkg>/<pkg>-<targetver>.tgz
|
||||||
|
# → republish <pkg>@<targetver> из source проходит, packument регенерится корректно
|
||||||
|
```
|
||||||
|
|
||||||
|
Рестарт контейнера **не нужен**: local-storage читает packument/тарболл с диска пер-реквест;
|
||||||
|
packument не трогаем, только убираем лишний `.tgz`, которого в индексе и так нет.
|
||||||
|
|
||||||
|
**Trade-off:** промежуточные версии (тарболлы на диске, но не в индексе после republish) остаются
|
||||||
|
orphaned — безвредны, не ставятся по pin, GC их не трогает (locally-published). Если нужна полная
|
||||||
|
история в индексе — отдельный скрипт-reconstruct packument из всех on-disk тарболлов
|
||||||
|
(`tar xzO package/package.json` per tgz + shasum/integrity) — heavier, по умолчанию не делаем.
|
||||||
|
|
||||||
|
## Применено 2026-06-11
|
||||||
|
|
||||||
|
`@snollajs/{data@0.9.0, mailer@0.7.4, numbering@0.7.4, content-api@0.8.1}` — удалены 4
|
||||||
|
коллизирующих тарболла, бэкап `/tmp/verdaccio-snollajs-rescue-20260611T140801Z.tar.gz` на
|
||||||
|
[[../entities/vds-kzntsv]]. Republish делегирован владельцу пакетов (сессия snolla).
|
||||||
|
Контекст: восстановление verdaccio после wipe (см. также [[verdaccio-token-lifecycle]] — другой
|
||||||
|
класс отказа того же restore-эпизода).
|
||||||
|
|
||||||
|
## Связанные
|
||||||
|
|
||||||
|
- [[verdaccio-prune-semantics]] — locally-published vs proxied, почему `rm` тарболла = permanent loss
|
||||||
|
- [[verdaccio-token-lifecycle]] — restart trap / ephemeral secret (соседний симптом того же restore)
|
||||||
|
- [[../entities/vds-kzntsv]] — хост
|
||||||
@@ -110,5 +110,7 @@ Refresh токена = обновить `npmAuthToken` в `.yarnrc.yml` кажд
|
|||||||
|
|
||||||
## Связанные страницы
|
## Связанные страницы
|
||||||
|
|
||||||
|
- **Общая вики:** `concepts/verdaccio-token-usage` (репо `projects-wiki`) — практический runbook аутентификации: два гейта, конфиг клиента, диагностика 401, stale-parent-process trap, чеклист. Эта страница — детальный postmortem про *почему*; runbook — про *как пользоваться*.
|
||||||
- [verdaccio-prune-semantics](verdaccio-prune-semantics.md) — storage layout и GC
|
- [verdaccio-prune-semantics](verdaccio-prune-semantics.md) — storage layout и GC
|
||||||
|
- [yarn-npm-minimal-age-gate](yarn-npm-minimal-age-gate.md) — гейт #2: YN0016 на свежих версиях (Yarn ≥4.16), независим от auth
|
||||||
- [vds-kzntsv](../entities/vds-kzntsv.md) — хост где крутится инстанс
|
- [vds-kzntsv](../entities/vds-kzntsv.md) — хост где крутится инстанс
|
||||||
|
|||||||
84
.wiki/concepts/yarn-npm-minimal-age-gate.md
Normal file
84
.wiki/concepts/yarn-npm-minimal-age-gate.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: Yarn 4.16 npmMinimalAgeGate — YN0016 "quarantined" на свежих пакетах
|
||||||
|
type: concept
|
||||||
|
tags: [yarn, yarn-berry, verdaccio, npm, supply-chain, gotcha, YN0016, age-gate]
|
||||||
|
sources: []
|
||||||
|
updated: 2026-06-11
|
||||||
|
---
|
||||||
|
|
||||||
|
# Yarn 4.16 `npmMinimalAgeGate` — клиентский «карантин» свежих версий
|
||||||
|
|
||||||
|
## Симптом
|
||||||
|
|
||||||
|
`yarn install` (Yarn Berry **≥4.16**) падает на резолве:
|
||||||
|
|
||||||
|
```
|
||||||
|
@snolla/site-schema-gen@npm:^0.5.0: All versions satisfying "^0.5.0" are quarantined
|
||||||
|
```
|
||||||
|
|
||||||
|
Код ошибки — **YN0016**. Версия физически есть в реестре, `dist-tags.latest` указывает на неё, токен валиден (`whoami` отдаёт юзера) — но yarn её не берёт.
|
||||||
|
|
||||||
|
## Root cause — клиентский age-gate, НЕ сервер
|
||||||
|
|
||||||
|
Yarn 4.16 ввёл настройку **`npmMinimalAgeGate`** (supply-chain мера против свежезалитых вредоносных версий, по следам атак на npm 2025). Из бинаря `~/AppData/Local/node/corepack/v1/yarn/4.16.0/yarn.js`:
|
||||||
|
|
||||||
|
```
|
||||||
|
npmMinimalAgeGate: { type: "DURATION", unit: "m", default: "1d" } # = 1440 минут
|
||||||
|
```
|
||||||
|
|
||||||
|
Фильтр перед резолвом отсеивает версию, если:
|
||||||
|
- у неё нет даты публикации в packument-поле `time`, **или**
|
||||||
|
- `(now − publishDate) < npmMinimalAgeGate`.
|
||||||
|
|
||||||
|
Если после фильтра кандидатов не осталось → `throw YN0016 "...are quarantined"`.
|
||||||
|
|
||||||
|
**Карантин целиком на стороне yarn-клиента.** На сервере verdaccio никакого карантина нет (нет в `config.yaml`, plugins-папка пустая) — серверная проверка пакумента его не видит. Реестр отдаёт версию чисто любому валидному read-токену.
|
||||||
|
|
||||||
|
## Версионная разница (почему «у одних работает»)
|
||||||
|
|
||||||
|
| yarn | `npmMinimalAgeGate` default |
|
||||||
|
|---|---|
|
||||||
|
| ≤ 4.14 (напр. snolla 4.14.1) | `0` — гейта нет |
|
||||||
|
| ≥ 4.16 | `1d` (1440 мин) |
|
||||||
|
|
||||||
|
Проект на 4.14 «просто работает», на 4.16 тот же канон ловит YN0016 первые 24 ч после публикации пакета.
|
||||||
|
|
||||||
|
## Два независимых последовательных гейта
|
||||||
|
|
||||||
|
Важно не путать с auth — это разные стены, в таком порядке:
|
||||||
|
|
||||||
|
1. **auth** — кривой/протухший токен → `YN0041 Invalid authentication` (это 401, см. [[verdaccio-token-lifecycle]]).
|
||||||
|
2. **age-gate** — валидный токен, но версия моложе порога → `YN0016 …quarantined`.
|
||||||
|
|
||||||
|
Решающий признак: если на **валидном** токене (auth прошёл, 401 нет) всё равно падает — это гейт #2, не auth. Token-fix его НЕ снимает.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .yarnrc.yml — глобально:
|
||||||
|
npmMinimalAgeGate: 0
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
# либо разово, per-invocation (сохраняет защитный дефолт для обычных install'ов):
|
||||||
|
YARN_NPM_MINIMAL_AGE_GATE=0 yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Нюанс: гейт читается ГЛОБАЛЬНО, не per-scope
|
||||||
|
|
||||||
|
В фильтре — `configuration.get("npmMinimalAgeGate")` (глобальный config), не scope-aware lookup. Поэтому `npmMinimalAgeGate` под `npmScopes.@snolla.*` **игнорируется** — нельзя ослабить гейт только для доверенного приватного скоупа. Глобальный `0` снимает age-защиту и с **публичных** npmjs-пакетов, которые verdaccio проксирует через uplink. Это реальный security trade-off — в этом и смысл фичи.
|
||||||
|
|
||||||
|
**Рекомендация:** не коммитить `0` в общий канон бездумно. Для CI / чужих проектов — разовый `YARN_NPM_MINIMAL_AGE_GATE=0` под осознанный install сразу после своей публикации. Для тесного publish→consume dev-loop своих пакетов (напр. pilonuxt) глобальный `0` оправдан — реестр один доверенный, DX важнее. Финальный risk-call — за владельцем экосистемы.
|
||||||
|
|
||||||
|
## Ecosystem heads-up
|
||||||
|
|
||||||
|
Любой потребитель на **yarn ≥4.16**, тянущий свежеопубликованные `@snolla`/`@snollajs` пакеты, будет ловить YN0016 первые 24 ч. Когда snolla-монореп/проекты переедут на 4.16+ — решить, добавлять ли `npmMinimalAgeGate: 0` в snolla-канон `.yarnrc.yml` или держать override on-demand.
|
||||||
|
|
||||||
|
## Применено / источник
|
||||||
|
|
||||||
|
Диагностировано 2026-06-11 совместно с сессией pilonuxt (миграция pnpm→yarn 4.16). Первичная ошибочная гипотеза «карантин = серверный дефект verdaccio» опровергнута: пруф — строка `are quarantined` + `npmMinimalAgeGate` в `yarn.js` 4.16.0, и YN0016 воспроизводится на валидном vitya-токене, снимается только `npmMinimalAgeGate: 0`.
|
||||||
|
|
||||||
|
## Связанные
|
||||||
|
|
||||||
|
- [[verdaccio-token-lifecycle]] — гейт #1 (auth / JWT / 401), соседняя стена
|
||||||
|
- [[verdaccio-restore-packument-desync]] — почему версии вообще пришлось republish (disaster-restore)
|
||||||
|
- [[../entities/vds-kzntsv]] — хост verdaccio
|
||||||
@@ -50,7 +50,9 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
|
|||||||
- [vbox-windows-stability-tuning](concepts/vbox-windows-stability-tuning.md) — VirtualBox + Windows-гость — нюансы стабильности cross-hypervisor миграции
|
- [vbox-windows-stability-tuning](concepts/vbox-windows-stability-tuning.md) — VirtualBox + Windows-гость — нюансы стабильности cross-hypervisor миграции
|
||||||
- [vds-kzntsv-dhcp-outage-2026-05-28](concepts/vds-kzntsv-dhcp-outage-2026-05-28.md) — DHCP outage 2026-05-28 на vds-kzntsv — диагностика + recovery runbook (link UP но без IPv4 → статика + тикет хостеру)
|
- [vds-kzntsv-dhcp-outage-2026-05-28](concepts/vds-kzntsv-dhcp-outage-2026-05-28.md) — DHCP outage 2026-05-28 на vds-kzntsv — диагностика + recovery runbook (link UP но без IPv4 → статика + тикет хостеру)
|
||||||
- [verdaccio-prune-semantics](concepts/verdaccio-prune-semantics.md) — Verdaccio prune semantics — proxied vs locally-published
|
- [verdaccio-prune-semantics](concepts/verdaccio-prune-semantics.md) — Verdaccio prune semantics — proxied vs locally-published
|
||||||
|
- [verdaccio-restore-packument-desync](concepts/verdaccio-restore-packument-desync.md) — disaster-restore: тарболл на диске + древний packument → EEXISTS 409; fix = снять коллизирующий .tgz, не rm -rf каталог
|
||||||
- [verdaccio-token-lifecycle](concepts/verdaccio-token-lifecycle.md) — restart trap (ephemeral secret) + max_users:-1 pnpm 409 + JWT fix + yarn clients
|
- [verdaccio-token-lifecycle](concepts/verdaccio-token-lifecycle.md) — restart trap (ephemeral secret) + max_users:-1 pnpm 409 + JWT fix + yarn clients
|
||||||
|
- [yarn-npm-minimal-age-gate](concepts/yarn-npm-minimal-age-gate.md) — Yarn ≥4.16 client-side age-gate (YN0016 «quarantined») на версиях <24ч; глобальный, не per-scope; fix npmMinimalAgeGate:0
|
||||||
- [winacme-iis-owin-catchall-http01](concepts/winacme-iis-owin-catchall-http01.md) — win-acme HTTP-01 авто-renewal LE на IIS под OWIN-catch-all CMS — challenge в отдельном No-Managed-Code приложении + патч шаблона; заменяет ручной PFX-импорт
|
- [winacme-iis-owin-catchall-http01](concepts/winacme-iis-owin-catchall-http01.md) — win-acme HTTP-01 авто-renewal LE на IIS под OWIN-catch-all CMS — challenge в отдельном No-Managed-Code приложении + патч шаблона; заменяет ручной PFX-импорт
|
||||||
- [wd40efax-smr-cascade](concepts/wd40efax-smr-cascade.md) — WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
|
- [wd40efax-smr-cascade](concepts/wd40efax-smr-cascade.md) — WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
|
||||||
- [windows-server-2025-core-bootstrap](concepts/windows-server-2025-core-bootstrap.md) — Win Server 2025 Core (RUVDS) — bootstrap для IIS-хоста: default-blockers + transfer-методов матрица
|
- [windows-server-2025-core-bootstrap](concepts/windows-server-2025-core-bootstrap.md) — Win Server 2025 Core (RUVDS) — bootstrap для IIS-хоста: default-blockers + transfer-методов матрица
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Append-only log of wiki operations (ingests, promotions, lints, migrations).
|
Append-only log of wiki operations (ingests, promotions, lints, migrations).
|
||||||
|
|
||||||
|
## [2026-06-11] ingest | verdaccio-restore-packument-desync — postmortem: disaster-restore вернул тарболлы но древний/пустой packument → publish свежей версии EEXISTS 409. Fix: снять только коллизирующий целевой .tgz (бэкап first), republish; НЕ rm -rf каталог (убьёт locally-published историю). Применено к @snollajs/{data,mailer,numbering,content-api}.
|
||||||
## [2026-06-11] ingest | verdaccio-token-lifecycle — restart trap (ephemeral secret → 401), max_users:-1 + pnpm 409 постмортем, JWT config fix, yarn classic vs berry token refresh recipe.
|
## [2026-06-11] ingest | verdaccio-token-lifecycle — restart trap (ephemeral secret → 401), max_users:-1 + pnpm 409 постмортем, JWT config fix, yarn classic vs berry token refresh recipe.
|
||||||
|
## [2026-06-11] ingest | yarn-npm-minimal-age-gate — Yarn ≥4.16 client-side supply-chain gate: версии <24ч → YN0016 «quarantined». Независим от auth (gate #2 после 401). default 1d, читается глобально (per-scope не работает), fix npmMinimalAgeGate:0 / YARN_NPM_MINIMAL_AGE_GATE=0. Диагностировано с pilonuxt; опровергнута первичная гипотеза «серверный карантин verdaccio».
|
||||||
## [2026-06-11] update | MSSQL backup gap closed — `/opt/stacks/backup/scripts/run.sh` расширен: 5 prod DBs через `docker exec mssql sqlcmd BACKUP DATABASE ... WITH COPY_ONLY, INIT` (Express, без COMPRESSION); .bak в bind-mount → mv в DUMP_DIR → rsync kreknin. Обновлён [[concepts/mssql-on-vds]] (TODO→реализовано).
|
## [2026-06-11] update | MSSQL backup gap closed — `/opt/stacks/backup/scripts/run.sh` расширен: 5 prod DBs через `docker exec mssql sqlcmd BACKUP DATABASE ... WITH COPY_ONLY, INIT` (Express, без COMPRESSION); .bak в bind-mount → mv в DUMP_DIR → rsync kreknin. Обновлён [[concepts/mssql-on-vds]] (TODO→реализовано).
|
||||||
|
|
||||||
## [2026-06-11] lint | 10 issues found, 8 fixed inline
|
## [2026-06-11] lint | 10 issues found, 8 fixed inline
|
||||||
|
|||||||
Reference in New Issue
Block a user