--- title: Verdaccio token lifecycle — restart trap + JWT fix type: concept tags: [verdaccio, npm, pnpm, yarn, auth, jwt, gotcha, postmortem] sources: [] updated: 2026-06-11 --- # Verdaccio token lifecycle — restart trap + JWT fix ## Root cause: auto-generated secret Verdaccio без явного `secret:` в конфиге генерирует случайный секрет **при каждом запуске**. Все токены (legacy opaque и JWT) подписаны этим секретом — перезапуск контейнера = мгновенная инвалидация всех выданных токенов у всех клиентов. **Симптом:** `401 Unauthorized` после рестарта VDS/контейнера, несмотря на то что токен в `.npmrc` визуально «есть». **Фикс (применён 2026-06-11):** явный `secret:` в `config.yaml` → токены переживают рестарты. ## `max_users: -1` + pnpm login → 409 Стандартный workaround «перелогинься» (`pnpm login --registry ...`) не работает при `max_users: -1`. Verdaccio htpasswd plugin v6 проверяет `max_users` **первым** в `adduser()`, до проверки существования пользователя: ``` if (max_users === -1) → return 409 "user registration disabled" ``` `pnpm login` отправляет `PUT /-/user/org.couchdb.user:` — это registration-path. Verdaccio блокирует даже для существующих юзеров, даже если пароль верный. Изменение `max_users` на положительное число решает проблему, но открывает регистрацию всем — нежелательно. ## Правильный способ получить токен при `max_users: -1` Использовать **web-UI login endpoint**, который не проходит через `adduser()`: ```powershell $body = '{"username":"vitya","password":"..."}' $r = Invoke-RestMethod -Uri "https://verdaccio.kzntsv.site/-/verdaccio/sec/login" ` -Method POST -Body $body -ContentType "application/json" $r.token # JWT, 7d expiry ``` Полученный токен — в `~/.npmrc`: ```ini //verdaccio.kzntsv.site/:_authToken=eyJ... ``` ## Конфиг-фикс (2026-06-11) Добавлено в `/opt/stacks/verdaccio/config/config.yaml` на [vds-kzntsv](../entities/vds-kzntsv.md): ```yaml secret: <32-byte-hex-permanent> security: api: jwt: sign: expiresIn: 30d notBefore: 0 web: sign: expiresIn: 7d verify: {} ``` До фикса: нет `secret` и нет `security` → legacy tokens, привязанные к ephemeral secret. После: web-UI и npm-API используют один JWT формат, подписанный постоянным секретом. > **Примечание:** `security.api.jwt` меняет формат API-токенов с legacy opaque (`5a3X...==`) на JWT (`eyJ...`). Старые legacy-токены в `.npmrc` у всех клиентов перестают работать после смены конфига — нужен refresh. ## Срок жизни токенов | Endpoint | Expiry | Используется для | |---|---|---| | `/-/verdaccio/sec/login` | 7d (web.sign) | Ручной refresh, агенты | | npm API (pnpm/yarn/npm login) | 30d (api.jwt.sign) | После решения `max_users` проблемы | ## Клиенты: где хранится токен ### npm / pnpm / yarn classic (1.x) Читают `~/.npmrc` (user-level) или `.npmrc` в корне проекта: ```ini //verdaccio.kzntsv.site/:_authToken=eyJ... @snollajs:registry=https://verdaccio.kzntsv.site/ ``` Yarn classic читает тот же `~/.npmrc` — update токена в одном файле покрывает всех. ### yarn berry (2.x+) Не читает `~/.npmrc`. Конфиг в `.yarnrc.yml`: ```yaml npmRegistries: "https://verdaccio.kzntsv.site": npmAuthToken: "eyJ..." npmAlwaysAuth: true npmScopes: snollajs: npmRegistryServer: "https://verdaccio.kzntsv.site" ``` Refresh токена = обновить `npmAuthToken` в `.yarnrc.yml` каждого проекта, либо использовать `yarn npm login --scope snollajs` (тоже обходит `max_users: -1` через web UI path). ## Связанные страницы - **Общая вики:** `concepts/verdaccio-token-usage` (репо `projects-wiki`) — практический runbook аутентификации: два гейта, конфиг клиента, диагностика 401, stale-parent-process trap, чеклист. Эта страница — детальный postmortem про *почему*; runbook — про *как пользоваться*. - [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) — хост где крутится инстанс