# GemTD: сервер, команда и соревнование 0.3.0

Работают профиль по почте, серверная одиночная партия, сохранение команд/состояния и история завершённых серверных партий. Совместная карта1–4 и Last Survivor2–4 включены и проверены локально; рейтинг ещё не включён. Сайт на Sites сохраняет прежнюю локальную игру; отдельный Node-сервер не запускается внутри статического размещения Sites.

## Локальный запуск

Нужны Node 24.15+ и pnpm 11.25.0. Из корня игры выполните `node scripts/build.mjs`, затем в каталоге `server` — `pnpm install --frozen-lockfile` и `pnpm dev`. Откройте `http://127.0.0.1:4198/`. Порт4190 браузеры блокируют, поэтому он не используется.

Без DATABASE_URL используется Electric PGlite: PostgreSQL в WASM только для разработки. База и случайные ключи сохраняются в исключённом из Git каталоге `.data`. Не запускайте два процесса с одной файловой базой PGlite. Полноценный PostgreSQL выбирается через DATABASE_URL; операции внутри транзакции используют один выделенный pg.Client.

Разработка не отправляет реальные письма без SMTP_URL. Тестовые сообщения находятся только в `.mailbox`, не доступны через HTTP и удаляются после10 минут. В production локальный почтовый ящик и PGlite запрещены. Код не возвращается API и не печатается в журнал.

## Профиль

- Регистрация: email → одноразовый код → имя/пароль. Повторный вход: email+пароль. Восстановление использует отдельное назначение кода.
- Код: криптографические6 цифр,10 минут,5 попыток; resend60с,5/час/email,20/час/IP,200/час/сервис. Лимиты конфигурировать после проверки доставляемости. Сохранён HMAC с отдельным ключом; почтовый outbox зашифрован AES-GCM.
- Пароль:15–128 Unicode-символов, Argon2id19MiB/2 прохода/1 поток; до4 одновременных вычислений и16 ожидающих запросов. В production проверяется Pwned Passwords: отправляется только5-символьный префикс SHA-1 с padding. При недоступности проверки новый пароль не принимается. В разработке действует только небольшой локальный список слабых паролей.
- Сессии:30 дней абсолютного срока,7 дней неактивности, HMAC токена в базе; Secure/HttpOnly/SameSite cookie в production. CSRF и строгий Origin для изменений. Восстановление отзывает все прежние сессии. Выход закрывает сокеты текущего процесса; внешние изменения сессии проверяются не реже5с.
- Блокировки по адресу сериализуют выдачу, проверку и применение кодов; повторное подтверждение/grant не создаёт второй аккаунт. Одинаковые ответы начала регистрации/сброса скрывают наличие аккаунта.

## Серверная партия

Только сервер исполняет Game, RNG, бой и экономику. Клиент получает read-only представление и отправляет допустимую команду. Пользователь/слот берутся из сессии, не из тела сообщения. Протокол1: roomId, epoch, UUID команды, clientSeq, expectedRevision, payload. Запрещены произвольные поля золота/HP и клиентские tick/RNG.

Команда подтверждается после одной транзакции: журнал+последовательность+snapshot. Повтор UUID/содержимого возвращает прежний результат; другой payload с этим UUID отклоняется. Ошибки правил тоже записываются и потребляют свой seq; нарушение порядка — нет. Lease30с и возрастающий epoch отсекают старого писателя. При ошибке хранения изменения и RNG откатываются к последней долговечной границе, actor блокируется до переподключения.

Симуляция30Hz, передача изменений10Hz. Пока используется простой долговечный snapshot каждые3 ticks активной комнаты — это консервативная первая реализация, **не** заявленная оптимальная производительность. У тихой комнаты без подключений стройка сохраняется раз в секунду; после5 минут такой actor выгружается. Бой после отключения продолжается до конца волны. Падение процесса останавливает расчёт до восстановления, пропущенное реальное время не догоняется. Автоматическое восстановление всех брошенных комнат и эксплуатационная политика ещё нужны.

Сокет подключается одноразовым билетом30с, переданным первым сообщением, без секрета в URL. Частота команд ограничена. Полное состояние отправляется при входе, затем только изменённые верхние поля; маршруты врагов сокращаются до направления для отображения. Будущие seed/RNG и внутренний журнал не рассылаются. Базовый генератор ядра пока32-битный: для будущего ranked нужна отдельная проверка предсказуемости и серверный поток с криптографическим ключом.

После50 сервер сохраняет выбор: продолжить endless или явно записать кампанию. «Завершить сетевую партию» до этого означает сдачу. Финализация unique по roomId; статистика не дублируется. Все текущие результаты — `server_casual`, глобального рейтинга/сезонных очков нет. Локальная история не импортируется как серверная.

## Совместная карта0.9.0

Лобби1–4, приглашение поURL, общая карта, отдельные пятёрки/RNG/способности, готовность и3с отсчёт. При запуске фиксируется P: HP умножаются на P, число/скорость/штрафы нет. Начальные булыжники32/16/8/0 — подмножество solo-позиций. Общее управление сохранёнными башнями задаётся до старта. Новые пятёрки всегда личные. Расчёт и произвольный ввод остаются только на сервере.

Протокол дополнен expectedTurn: запоздалая постановка не может войти в следующий ход или повтор пятёрки. Синтез, обмен, удаление и плита требуют точного expectedRevision. Команда, membership и snapshot сохраняются последовательно одним actor. Join/leave лобби сохраняют seq; повторный вход не создаёт второй слот.

Готовность отключившегося отменяется. После90с сервер без покупок выбирает его первую башню, сохраняя исходную сложность; после300с без всех подключений — abandoned. Создатель при сдаче передаёт руководство. В финале один командный результат и отчёт каждого; общая экономика явно помечена shared_team.

Проверено287 клиентских+34 серверных теста, включая четыре HTTP/WebSocket-сеанса и прохождение жизненного цикла50 волн с принудительным уроном. Браузерный сценарий tools/qa/coop-ui.mjs проверяет4 независимых профиля,20 камней, переподключение, подсветку и общий старт. Это не нагрузочная проверка и не оценка баланса.

## Last Survivor0.10.0

Профиль last-survivor-equal-v1,2–4 независимых поля. Общая стройка60с, отсчёт3с; неготовую пятёрку автоматически завершает сервер без покупок. Ранний финиш ждёт остальных; после50 при нескольких оставшихся — автоматический endless. Все поля рассчитываются перед решением об одновременном выбывании; ничья не назначает победителя.

HMAC-ключ матча остаётся в закрытом snapshot. Базовые броски привязаны к wave/rerollOrdinal/drawOrdinal; молитва и понижение качества не сдвигают следующий бросок. Результат качества зависит от личного опыта. Исходные противники/элита/штрафы одинаковые, счёт обычной волны10, endless30. Это отдельный веб-профиль, не native Dota Race.

Snapshotformat3 хранит все поля. Сервер отсылает только собственные структуры и собственные события, плюс публичный счёт соперников. Сохраняются один общий итог и личные отчёты с местом и личной экономикой. Хозяин может выбыть раньше других, не завершая комнату. Отложенные команды проверяются по личному turnId/revision.

Проверены модель, SQL-откат/восстановление,4 настоящих WebSocket-клиента и4 независимых браузерных профиля. Браузерный финал — сдача тестовых соперников, не стратегическая победа. Скрипт: tools/qa/survivor-ui.mjs в основном рабочем проекте. Подробности: plan/54_LAST_SURVIVOR.md.

## Проверки

`pnpm test` проверяет SQL через PGlite, auth, настоящий HTTP/WebSocket, потерю записи, lease/epoch, повтор команд, выдачу результата. Отдельная проверка действительно убивает дочерний Node-процесс и открывает файловую базу заново: сессия/камень/команда восстановлены после истечения lease. Переход тестовых часов на31с не является измерением реального времени восстановления.

Внешний UI-проверяющий сценарий `tools/qa/server-profile-ui.mjs` находится в корневом рабочем проекте. Он использует тестовую почту и реальную карту на320×568,390×844,844×390,1440×1000. Не измеряет FPS настоящего телефона.

## Подготовка размещения

Это **подготовленный, ещё не запущенный** Docker/Compose-контур. Скопируйте `.env.example` в `.env` локально и заполните значения приватно. Для production уберите DEV_DATABASE, задайте NODE_ENV=production, HTTPS-адрес и три разных ключа. Пароли БД в примере — различные случайные hex-строки, безопасные для URL. Команда из корня игры: `docker compose --env-file server/.env -f server/deploy/compose.yaml up --build -d`.

Node и клиент обслуживаются с одного origin за HTTPS-прокси, который передаёт WebSocket upgrade и корректный X-Forwarded-For. Порт4198 наружу не открывается; PostgreSQL тоже не публикуется. TRUST_PROXY допускается только за настроенным доверенным прокси. Отправитель SMTP должен быть подтверждён; SPF/DKIM/DMARC настраиваются у выбранного сервиса. Не отключайте проверку TLS.

Приложение использует отдельную роль БД без superuser. Образы Node24/PostgreSQL17 перед реальным запуском закрепить проверенными digest и проверить восстановление резервной копии. Docker здесь отсутствует: сборка образа, полный PostgreSQL, два независимых писателя, reverse proxy, SMTP-доставка и нагрузка **ещё не проверены**. Не подключать ranked/оплаты до этих проверок.

Источники: [node-postgres transactions](https://node-postgres.com/features/transactions), [Electric PGlite API](https://pglite.dev/docs/api), [ws](https://github.com/websockets/ws), [node-argon2](https://github.com/ranisalt/node-argon2), [OWASP Password Storage](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html), [Pwned Passwords](https://haveibeenpwned.com/API/v3#PwnedPasswords), [Nodemailer SMTP](https://nodemailer.com/smtp).
