Skip to content

0066. npm-реестр для Unity Package Manager ​

Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-04. Третий протокол рядом с /api/v1 после реестра образов (ADR 0063) и Git LFS (ADR 0065); хранение и права — прежние.

Контекст ​

Unity подключает пакеты из «scoped registries» — серверов с протоколом npm. Студии держат в них свои SDK, инструменты и общие модули, которые проекты подтягивают по версиям. Unity требует от такого сервера метаданные пакета, tarball и поиск (/-/v1/search или /-/all); имена со @scope Unity не поддерживает. Публикуют пакеты обычным npm publish. Тот же протокол нужен для JavaScript-инструментов сборки, поэтому реестр npm-совместимый, а не только «для Unity».

Решение ​

Адрес. Реестр репозитория — https://<host>/npm/<repository>/. В Unity это url scoped registry в Packages/manifest.json, в npm — registry или @scope:registry в .npmrc.

Протокол — подмножество npm registry API:

  • GET <name> — packument. Каждая версия — её package.json с name, version, _id и dist реестра (tarball, shasum SHA-1, integrity SHA-512).
  • GET <name>/-/<file>.tgz — tarball с Range, через общую очередь и ограничение полосы.
  • PUT <name> — npm publish: одна версия и её tarball в base64.
  • GET /-/v1/search?text=&from=&size= — поиск по части имени или описания и по ключевому слову. В ответе по одной строке на пакет: версия latest, иначе новейшая.
  • GET|PUT|DELETE /-/package/<name>/dist-tags[/<tag>] — npm dist-tag. latest удалить нельзя.
  • Имена — правила npm (строчные, до 214 символов, @scope/ допустим для npm-клиентов). Версии — SemVer 2.0. Тег не может выглядеть как версия.
  • Unpublish, deprecate, npm login и /-/all не поддерживаются. Изменения по этим путям получают 405.
  • Ошибки — {error, code, request_id}: npm печатает error после строки статуса.
  • Маршрут /npm/:repository/* следует протоколу npm и в OpenAPI /api/v1 не входит.

Публикация потоком. Тело npm publish — JSON с tarball в base64, а пакеты Unity бывают по сотням мегабайт. Сервер разбирает тело потоком:

  • строка _attachments.<file>.data декодируется из base64 сразу в staging raw-файлов (ADR 0064);
  • остальной документ держится в памяти до 8 МиБ;
  • второй tarball, экранирование или ошибка base64 отклоняются.

Затем tarball читается заново:

  • считаются SHA-1, SHA-256 и SHA-512 всех байтов;
  • из gzip+tar извлекается <каталог>/package.json, как это делает npm. Распаковка останавливается на нём; zlib работает вне event loop и с backpressure.

Манифест версии — package.json из tarball, а не JSON клиента. Имя и версия в нём обязаны совпасть с публикацией. Длина, shasum и integrity, которые объявил клиент, сверяются с полученными байтами (integrity_mismatch). Зеркало выводит ту же строку из тех же байтов.

Неизменяемость.

  • Опубликованная версия не меняется.
  • Тот же tarball повторно — успех без изменений: так повтор после обрыва безопасен. Сам npm до отправки отказывает, если версия уже есть.
  • Другое содержимое той же версии — 409 version_exists.
  • Tarball — артефакт с меткой npm и ключом идемпотентности по SHA-256.
  • Строку версии заменяет только новая публикация, если её артефакт удалён администратором.

Вход. Ключ Arkvory как токен: Bearer из _authToken npm или token в .upmconfig.toml Unity. Также принимается Basic (_auth) с ключом как паролем. Ответ 401 не содержит вызова: клиенты шлют настроенный токен сами.

Права:

  • packument и tarball — content.read;
  • поиск и список тегов — artifact.list;
  • публикация и изменение тегов — upload.create (файловые ключи — write);
  • токен только для чтения и зеркало ничего не меняют;
  • шлюзы чтения (ADR 0009) обслуживают установку: все чтения — GET.

Хранение.

  • Миграция 32 (только расширение схемы): arkvory_npm_versions(repository, name, version → artifact, file, manifest, description, keywords, shasum, integrity, published_at) и arkvory_npm_tags(repository, name, tag → version).
  • Манифест хранится как текст: jsonb отвергает часть допустимых строк JSON.
  • Чтения пропускают версии, артефакт которых недоступен.
  • Обе таблицы входят в резервные копии.
  • Retention не удаляет tarball версии (причина reference).

Зеркала. В ленту пишутся npm.version ({name, version}), npm.tag ({name, tag, version}) и npm.tag.delete ({name, tag}). Запись идёт в транзакции изменения, под замком каталога репозитория. Зеркало:

  • копирует артефакт;
  • выводит версию из его package.json и хешей;
  • применяет теги по имени, пропуская неизвестные версии;
  • после засева проигрывает эти записи, как записи реестра образов.

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

  • Удаления версий нет: потребители закрепляют версии в lock-файлах, и удаление ломает сборки. Неверную версию заменяет следующая.
  • Прокси к npmjs.org и upstream-реестрам нет: пакеты из публичного реестра Unity и npm берутся оттуда напрямую, а Arkvory хранит свои.
  • Поиск — подстрочный, без ранжирования по популярности.
  • time в packument зеркала — время применения на зеркале.

Проверки ​

  • tests/npm-packages.test.mjs:
    • разбор тела при любом размере кусков;
    • padding на границе кусков, экранированные ключи;
    • отказы: второй tarball, неверный base64, превышение документа, неверный JSON;
    • tar с pax-путём и пропуском больших записей, вложенный и отсутствующий package.json, повреждённый заголовок;
    • правила публикации, integrity, packument и деталей ленты.
  • tests/integration/npm-registry.test.mjs с настоящим npm:
    • publish, публикация с --tag beta, отказ npm на повтор;
    • на уровне сервера: повтор того же tarball — 200, другое содержимое — 409, неверный shasum — integrity_mismatch;
    • dist-tag add/rm/ls, install с проверкой integrity и сверкой файла, search;
    • поиск и packument так, как их читает Unity (unity, displayName);
    • пакет @team/util с %2f;
    • 401 и 404 в формате npm;
    • ключ только для чтения: install работает, publish и dist-tag — 403; unpublish — 405.
  • tests/integration/mirror-images.test.mjs: версии до и после засева, перенос и удаление тега, tarball с зеркала по dist.tarball, публикация в зеркало отклоняется. tests/mirror-sync.test.mjs: проигрыш npm.* и пропуск неверных деталей.
  • Настоящий клиент Unity в CI не запускается: его запросы (packument, tarball, /-/v1/search) проверены через HTTP по описанию Unity.

Дополнение: проверка перед 0.4.0 ​

  • Tarball читается до конца. npm при извлечении оставляет последний файл пути, поэтому второй package.json (в том числе x/./package.json) или ссылка на его месте — отказ. Иначе реестр проверял бы один манифест, а клиенты ставили бы другой. pax-заголовок больше 64 КиБ — отказ, а не пропуск.
  • Архив, который распаковывается больше чем в 100 раз сверх своего размера плюс 64 МиБ (gzip-бомба), отклоняется. Проверка останавливается вместе с запросом.
  • JSON тела публикации — не глубже 64 уровней. package.json — до 256 КиБ: packument отдаёт манифесты всех версий одним ответом.
  • Если загрузка под ключом npm-<sha256> истекла или отменена, публикация идёт под новым ключом.
  • Адреса tarball строятся так же, как ссылки Git LFS (ADR 0065): X-Forwarded-Proto только повышает схему.
  • При гонке двух публикаций одной версии с разным содержимым проигравший артефакт остаётся без ссылок, и его удаляет retention.

Arkvory · ProAnimaStudio