Верхняя граница пароля была объявлена в 64 СИМВОЛА и обоснована пределом bcrypt в 72 БАЙТА. Обоснование верно только для ASCII: у 64 символов длина от 64 до 256 байт. golang.org/x/crypto@v0.55.0 (bcrypt.go:96) отвечает на пароль длиннее 72 байт ErrPasswordTooLong, а не «молча отбрасывает остаток», как утверждал комментарий, — так вела себя редакция пакета до v0.28. Следствие: пароль из 64 кириллических букв (128 байт) проходил панель, оркестратор и DTO, а отказ приходил из хеширования — системной ошибкой на штатной смене пароля, а при установке падением старта админки, то есть сервером без администратора после INSTALL EXIT CODE: 0. Хуже самого дефекта было то, что тест закреплял это значение как ожидаемое. Вместе с ним закрыты три соседних расхождения того же контракта. Пароль триммился вопреки собственному контракту. util.HashPassword вёл проверку len(strings.TrimSpace(password)) < 6, а bootstrap читал strings.TrimSpace(os.Getenv("HY2XS_ADMIN_INITIAL_PASSWORD")). Значение "abcde " принимали все двери продукта и не мог захешировать никто, а первая учётная запись создавалась не с тем паролем, который оператор записал в hy2xs.env. Панель считала длину в единицах UTF-16. Element Plus делегирует правила формы async-validator, а он сравнивает min/max с String.prototype.length: пароль из трёх эмодзи имел length 6, проходил минимум формы и получал отказ сервера, который панель не могла объяснить. hy2xs.env не был форматом. Значения писались интерполяцией, а читались split("=") с trim(); при этом файл читает не только оркестратор — он объявлен EnvironmentFile= в юните hy2xs-admin, и у незакавыченного значения systemd срезает краевые пробелы и трактует обратный слеш как escape. Что сделано: - контракт переехал в leaf-пакет apps/credential: его зовут util.HashPassword и dao, а service импортирует util — обратный импорт был бы циклическим, и именно поэтому HashPassword завёл собственную копию правила; - AdminPasswordMaxBytes = 72 объявлен отдельной константой и зеркально в оркестраторе и панели; сверяется тестами, читающими Go-исходник; - одно правило adminPassword вместо min=6,max=64 в тегах DTO (границу в байтах тегом валидатора не выразить) и код причины admin_password_format, называющий обе границы; - TrimSpace убран из хеширования и из bootstrap-пути; bootstrap проверяет контракт сам и падает с текстом, называющим переменную и файл; - панель считает code points и UTF-8 байты общим adminPasswordFormRule на обеих формах вместо встроенных min/max; - orchestrator/src/lib/envFile.ts — порт конечного автомата parse_env_file_internal из systemd и обратный ему кодировщик; экранируются только обратный слеш и двойная кавычка, оба из SHELL_NEED_ESCAPE. Обычные значения остаются без кавычек, поэтому релизные гейты не меняются. Тем же кодировщиком пишется bootstrap-admin.secret; - управляющие символы запрещены контрактом: формат KEY=VALUE их не несёт, а ввести такой пароль в форму входа всё равно нельзя; - отрицательная проба smoke сверяет конверт отказа (code 50000, invalid_credentials, отсутствие accessToken) вместо HTTP 200, а пароль генерирует, а не берёт из литерала; - положительная проба читает bootstrap-секрет парсером формата вместо grep | cut -d= -f2- с trim() — третьего по счёту слоя, срезавшего пробелы. Тесты: граничная таблица (36 x «я», 37 x «я», 18 и 19 эмодзи, 64 x «я», «abcde ») прогоняется в четырёх слоях; тест с 64 кириллическими буквами инвертирован; round-trip env-формата на значениях с кавычками, слешами и краевыми пробелами; bootstrap-путь на настоящей SQLite. 14 новых гейтов приёмки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HY2XS production builder
Этот каталог содержит production builder для HY2XS.
Builder собирает один переносимый install-archive:
dist/hy2xs-install-<version>.tar.gz
Этот архив переносится на production server, распаковывается и устанавливается через install.sh.
На production server не должно быть сборки из исходников: без go build, без bun install, без pnpm install, без frontend build и без TypeScript transpilation.
Что где лежит
Основные части проекта:
versions.env— контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм.tools/build/build.sh— главный entrypoint сборки.tools/build/lib/versions.sh— загрузкаversions.envиverify_versions_contract.tools/build/lib/deps.sh— проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm.tools/build/lib/package.sh— сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива.tools/build/lib/verify.sh— проверка структуры репозитория и итогового архива.tools/build/lib/acceptance.sh— acceptance-проверки production-контракта.tools/build/lib/hysteria.sh— разрешение upstream-версии Hysteria и compatibility gate.tools/build/hysteria-lock.env— fallback-значения для офлайн-сборки (HYSTERIA_CHANNEL=pinned).tools/test/e2e-hysteria.sh— end-to-end проверка с реальным клиентом Hysteria.orchestrator— TypeScript/Bun install-only orchestrator.apps— HY2XS admin: Go backend и Vue frontend.package— skeleton будущего install package:install.sh, templates, systemd units, default config.dist— итоговые архивы. Создаётся builder'ом, в git обычно не хранится..toolchain— локальный toolchain builder'а. Создаётся автоматически, в git не хранится.
Требования к build machine
Production build поддерживается только на:
- Debian 13;
- amd64 / x86_64;
- root или пользователь с
sudo; - доступ в интернет.
Нужен outbound HTTPS/DNS до:
- Debian apt repositories;
go.dev;github.com;nodejs.org;- npm registry.
Windows и macOS можно использовать для редактирования исходников, но финальную production-сборку надо делать на Debian 13 amd64.
Что builder делает сам
При запуске builder:
- Загружает и валидирует
versions.env. - Проверяет, что host соответствует
HY2XS_BUILD_*(по умолчанию Debian 13 amd64). - Проверяет структуру репозитория.
- Устанавливает недостающие системные build dependencies через
apt-get. - Проверяет или скачивает локальные версии Go/Bun/Node.js/pnpm из контракта
и сверяет каждый архив с контрольной суммой из
versions.env. - Выполняет
verify_versions_contract: рассинхрон версий роняет сборку до создания tarball. - Прогоняет тесты и типы оркестратора (
bun test,tsc --noEmit). - Разрешает upstream-версию Hysteria, берёт ожидаемый SHA-256 из upstream
hashes.txtи сверяет с ним скачанный артефакт. - Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS.
- Копирует package skeleton.
- Собирает install-only orchestrator в standalone binary.
- Собирает frontend и backend HY2XS admin в Linux amd64 binary, проставляя версию админки через ldflags.
- Прогоняет
go vetиgo testдля HY2XS admin (после сборки frontend, потому чтоgo:embed all:distтребует готовых ассетов). - Записывает metadata и checksums.
- Создаёт
dist/hy2xs-install-<version>.tar.gz. - Проверяет архив и прогоняет acceptance-проверки.
Контракт версий
Версии продукта, платформы и toolchain объявлены в корневом
versions.env. Собственных значений по умолчанию у
deps.sh больше нет: без загруженного контракта сборка падает сразу.
Подход — проверка, а не генерация. profile.ts,
package/config/hy2xs.env и packageManager в обоих package.json остаются
обычными файлами, чтобы bun test, tsc и go test работали из чистого
чекаута до запуска сборки. verify_versions_contract сверяет их с контрактом и
роняет сборку при расхождении.
Контракт оркестратора сверяется не grep'ом по исходникам, а выводом
orchestrator/tools/print-contract.ts:
это доказывает, что в бинарь попало то же значение.
Версия админки приезжает в бинарь через ldflags
(-X 'hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}') и проверяется
запуском собранного hy2xs-admin version. Захардкоженной константы версии в
Go-коде больше нет: она уже успела разъехаться с версией пакета.
Чего в versions.env нет намеренно: прикладных зависимостей (для них есть
pnpm-lock.yaml, bun.lock, go.sum) и конкретной версии Hysteria (здесь
только политика HYSTERIA_CHANNEL, результат резолва — в
hysteria-lock.env).
Версия Hysteria: разрешение и compatibility gate
Builder не хранит версию Hysteria вручную. По умолчанию он определяет последнюю стабильную версию сам и замораживает её в пакете.
Правила разрешения:
- канонический upstream —
HyNetworks/hysteria; - принимаются только стабильные релизы, без draft и prerelease;
- тег должен иметь вид
app/vX.Y.Z; - берётся ровно один артефакт
hysteria-linux-amd64и ровно одинhashes.txt; - URL используется в том виде, в каком его вернул upstream API, без пересборки строки;
- ожидаемый SHA-256 берётся из upstream
hashes.txt, и скачанный бинарник сверяется с ним; - разрешённые значения попадают в metadata пакета.
Почему hashes.txt, а не локальный пересчёт
Раньше SHA-256 считался от уже скачанного файла. Это защищает target от последующей подмены, но не доказывает, что builder скачал именно ожидаемый upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было.
Формат ассета:
6493dfff…f94 build/hysteria-linux-amd64
f24f63be…189 build/hysteria-linux-amd64-avx
Сопоставление идёт по базовому имени и строго на равенство: build/ — часть
пути, а hysteria-linux-amd64-avx — другой артефакт, который не должен
совпасть по префиксу. Разбор вынесен в parseUpstreamHashes
(hysteriaRelease.ts) и
покрыт юнит-тестами.
Источник ожидаемой суммы фиксируется в metadata как hysteria_sha_source.
Сравнение версий числовое, поэтому v2.9.10 считается новее v2.9.2.
После разрешения обязателен compatibility gate:
скачать бинарник
↓
сверить SHA-256 и `hysteria version`
↓
отрендерить канонический конфиг HY2XS тем же кодом, что и на target
↓
запустить настоящий Hysteria с этим конфигом (gecko и salamander)
↓
только после этого собирать release package
При несовместимости сборка останавливается:
BUILD FAILED: unsupported Hysteria stable v2.13.0
Это осознанное решение: ошибка должна проявиться на build machine, а не на production-сервере.
Переменные:
| Переменная | По умолчанию | Назначение |
|---|---|---|
HYSTERIA_CHANNEL |
из versions.env (stable) |
stable — разрешить последнюю стабильную через upstream API; pinned — офлайн-сборка по hysteria-lock.env |
HYSTERIA_VERSION_OVERRIDE |
пусто | Закрепить конкретную версию vX.Y.Z |
HYSTERIA_COMPAT_GATE |
true |
Compatibility gate; для release-сборок обязателен |
HYSTERIA_VERIFY_UPSTREAM_HASHES |
true |
Сверять артефакт с upstream hashes.txt; отключение — только break-glass |
HYSTERIA_WRITE_LOCK |
false |
Записать разрешённые значения обратно в hysteria-lock.env |
HYSTERIA_GATE_PORT |
34443 |
UDP-порт для временного запуска Hysteria в gate |
HYSTERIA_GATE_STATS_PORT |
34712 |
TCP-порт trafficStats в gate |
GITHUB_TOKEN |
пусто | Опционально: снимает anonymous rate limit GitHub API |
Тесты, проверка типов и проверка зависимостей переменными не управляются: у них
нет аварийного выхода. Готовый пакет объявляет об этом полями tests_gate=true
и dependency_security_gate=true в metadata/package.env.
Обновить lock-файл под текущий upstream:
HYSTERIA_WRITE_LOCK=true ./tools/build/build.sh
Важное про Bun и старые CPU
Bun имеет два Linux x64 artifact'а:
bun-linux-x64— обычный build;bun-linux-x64-baseline— build для CPU без AVX2.
Если CPU не поддерживает AVX2, обычный Bun может падать с Illegal instruction.
Builder автоматически проверяет /proc/cpuinfo.
По умолчанию:
BUN_FLAVOR=auto
Логика:
- если CPU поддерживает AVX2, используется
bun-linux-x64; - если CPU не поддерживает AVX2, используется
bun-linux-x64-baseline.
Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В
versions.env зафиксированы обе:
BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>
Ожидаемый digest выбирается уже после select_bun_artifact().
Можно принудительно задать flavor:
BUN_FLAVOR=x64 ./tools/build/build.sh
или:
BUN_FLAVOR=x64-baseline ./tools/build/build.sh
Запуск
Из корня репозитория:
./tools/build/build.sh
Версия пакета берётся из versions.env, поэтому обычно нужен только build id:
BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh
Проверка результата
Проверка архива:
ls -lh dist/hy2xs-install-1.0.0.tar.gz
sha256sum dist/hy2xs-install-1.0.0.tar.gz | tee dist/hy2xs-install-1.0.0.tar.gz.sha256
Проверка обязательных файлов:
tar -tzf dist/hy2xs-install-1.0.0.tar.gz | grep -E '^(hy2xs-install/install.sh|hy2xs-install/orchestrator/hy2xs-orchestrator|hy2xs-install/ui/hy2xs-admin/hy2xs-admin|hy2xs-install/metadata/checksums.txt)$'
Проверка metadata:
tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env
Когда обновлять orchestrator/bun.lock
Не перегенерируйте orchestrator/bun.lock во время production-сборки.
Обновлять и коммитить orchestrator/bun.lock следует только когда:
- изменился
orchestrator/package.json; - изменился
BUN_VERSIONвversions.env; - зависимости оркестратора обновляются осознанно.
При смене BUN_VERSION нужно обновить и packageManager в
orchestrator/package.json, и обе контрольные суммы Bun: иначе
verify_versions_contract остановит сборку.
Production builder всегда выполняет:
bun install --frozen-lockfile
Если команда падает, исправьте и закоммитьте lockfile в системе контроля версий. Не убирайте --frozen-lockfile.
Дисциплина lockfile для frontend
Не перегенерируйте frontend lock data во время обычной production-сборки.
Правила управления пакетами frontend:
apps/frontend/package.jsonобъявляет"packageManager": "pnpm@9.15.9";- builder использует закреплённый pnpm из
PNPM_VERSIONвversions.env, иverify_versions_contractсверяет эти два значения; - production-путь установки frontend всегда:
pnpm install --frozen-lockfile
Если frozen install падает, обновите зависимости осознанно в системе контроля версий и закоммитьте изменения lockfile. Не убирайте --frozen-lockfile из сборочного потока.
Полезные переменные
BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ)BUN_FLAVOR=auto|x64|x64-baselineFRONTEND_NODE_OLD_SPACE_SIZE=2048(default memory limit for frontend build step)TOOLCHAIN_DIR=/custom/path/.toolchainVERIFY_TOOLCHAIN_CHECKSUMS=trueVERSIONS_ENV_FILE=/custom/path/versions.env
PACKAGE_VERSION берётся из versions.env (HY2XS_VERSION); переопределять
его вручную нужно только для отладочных сборок, и verify_versions_contract
такую сборку отклонит.
Контрольные суммы toolchain больше не передаются через окружение: они
объявлены в versions.env. Раньше воспроизводимая сборка в чистой Debian-среде
требовала предварительного знания четырёх SHA-256 и не запускалась одной
командой.
Политика памяти при сборке frontend
Builder задаёт безопасный лимит heap для Node.js внутри bundle_ui():
--max-old-space-size=2048
Способы переопределения:
- изменить значение по умолчанию для этой политики:
FRONTEND_NODE_OLD_SPACE_SIZE=3072 ./tools/build/build.sh
- или передать полный набор собственных Node-опций (если
--max-old-space-sizeтам уже задан, builder не добавит второй):
NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh
Диагностика
Проверить AVX2:
grep -qw avx2 <<<"$(grep -m1 '^flags' /proc/cpuinfo)" && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'
Проверить Bun:
.toolchain/bun/bin/bun --version
echo "bun_exit=$?"
Если есть Illegal instruction, удалите старый Bun и пересоберите:
rm -rf .toolchain/bun .toolchain/bun-tmp
rm -f .toolchain/downloads/bun-linux-x64-*.zip
rm -f .toolchain/downloads/bun-linux-x64-baseline-*.zip
./tools/build/build.sh
Проверить shell syntax:
bash -n tools/build/build.sh
bash -n tools/build/lib/common.sh
bash -n tools/build/lib/versions.sh
bash -n tools/build/lib/deps.sh
bash -n tools/build/lib/hysteria.sh
bash -n tools/build/lib/package.sh
bash -n tools/build/lib/verify.sh
bash -n tools/build/lib/acceptance.sh