- resolveChatName: чат с 2 участниками (я+один) и пустым title теперь
считается диалогом → имя собеседника вместо "CHAT <id>" (диалоги от
createDialog приходят с type:"CHAT").
- Временные [dbg]-логи: CHAT_UPDATE (не-реакции) и необработанные опкоды —
чтобы поймать, чем реально приходит удаление чата (CLOSED не сработал).
MSG_GET_REACTIONS реально отвечает {messagesReactions:{"<id>":{counters:[]}}}
(подтверждено кадром 2026-08-15), а getReactions парсил reactionInfo/reactions
— ни одна ветка не совпадала, функция ВСЕГДА возвращала []. Из-за этого
pollReactionRemovals раз в 60с считал, что реакция в MAX снята, и стирал её в
Telegram — отсюда «реакции со временем пропадают». Добавлен разбор реального
формата (по messageId, с фолбэком на единственную запись), старые формы
оставлены как фолбэки.
MAX отвергал реакцию ❤ (error.message.like.unknown.like), а 👍 принимал:
Telegram Bot API отдаёт часть реакций в text-форме без U+FE0F (❤ = U+2764),
а MAX хранит emoji-форму (❤️ = U+2764 U+FE0F). Это не разные наборы, а разное
представление одного эмодзи — чинится симметричной нормализацией selector'а,
а не таблицей соответствия.
Новый модуль max/reactions.ts: toMaxReaction (TG→MAX, добавляет FE0F нужным
базовым символам, включая внутри ZWJ-последовательностей вроде ❤🔥/🤷♂️) и
toTelegramReaction (MAX→TG, убирает FE0F — setMessageReaction хочет bare-форму).
Применено в addReaction, в релее реакций MAX→TG и в re-affirm зонда. Список
проблемных code points — из набора реакций Telegram (Emoji_Presentation=No).
Покрыто тестами (round-trip TG→MAX→TG).
Agent из npm-пакета undici v8 нельзя передавать во встроенный fetch Node 22:
у внутреннего undici другой интерфейс диспетчера, и каждый запрос падал с
"invalid onRequestStart method". Ломались обе стороны вложений: MAX→TG
приходили заглушки вместо файлов, TG→MAX выгрузка падала целиком (сообщение
не создавалось — отсюда и «/delete не находит связку»). Текст ходил, потому
что идёт по TCP-сокету мимо fetch. Поймано на тестовом сервере 2026-08-14,
воспроизведено и подтверждено локально: глобальный fetch + npm-Agent падает
с той же ошибкой, undici-fetch + тот же Agent работает.
FormData в upload.ts теперь тоже импортируется из undici — класс в рантайме
тот же, но типы npm-пакета номинально несовместимы с копией undici-types
из @types/node.
- redactSecrets: маскируются password/trackId/phone и любые строки от 512
символов — пароль MAX больше не попадает в лог веб-панели открытым текстом
- Веб-панель по умолчанию работает по HTTPS с самоподписанным сертификатом
(генерируется при первом старте в .data/tls); старые http-ссылки получают
301-редирект на том же порту (polyglot по первому байту; resume строго в
process.nextTick — иначе TLS-хендшейк зависает); /apikey даёт https-ссылку;
PANEL_TLS=off для установки за своим reverse proxy
- Сертификаты Минцифры больше не доверяются всему контейнеру через
NODE_EXTRA_CA_CERTS — src/max/ca.ts скоупит их только на соединения с MAX
(ca в tls.connect + undici-агент для CDN-загрузок); Telegram и GitHub
проверяются только по стандартным корням Mozilla
- Запросы одного опкода к MAX сериализуются (request() в client.ts) — два
конкурентных MSG_SEND больше не перепутают messageId-связки редактирования
и удаления
- Починена гонка первого чтения в ChatMapStore: конкурентные обращения
форкали кэш, и часть upsert'ов молча терялась на диске
- Graceful shutdown по SIGTERM/SIGINT; в панели — реконнект WebSocket и
периодическое обновление метрик; сравнение id в patchCachedChatLastMessage
переведено на String() (BigInt-чаты не совпадали)
- Удалён мёртвый код: эндпоинты /api/debug/*, getPollVoters,
createTelegramBot, цепочка historySynced, getChats(marker)
- React/Tailwind/lucide перенесены в devDependencies — рантайм-образ легче;
vitest 4, npm audit: 0 уязвимостей; chmod 600 для .env в setup.sh/update.sh
- Новые тесты: redactSecrets, сторы (эвикшн/нормализация/конкурентность),
сериализация запросов — всего 41
Full flow reverse-engineered and confirmed live: CHECK_CODE responds
with a passwordChallenge (trackId + optional hint) instead of a login
token when the account has a password set. A new opcode, CHECK_PASSWORD
(0x0073), exchanges {trackId, password} for the same 663-char login
token CHECK_CODE would have returned directly — from there it's an
ordinary LOGIN.
verifyCode() now returns a discriminated union (ok / password_required)
instead of throwing either way, so callers can actually ask for a
password instead of just failing. Wired through everywhere MAX auth
happens: the web panel gets a new password step (with hint), setup.sh's
console flow prompts for it too, and the dev auth-cli script handles it
via the same file-polling pattern it already used for the SMS code.
A wrong password doesn't burn the trackId (confirmed live) — both the
web UI and setup.sh let you retry it freely without a fresh SMS. It's
only consumed on success, matching MAX's own behavior.
Two-way message/media bridge between MAX and Telegram forum topics, with
group management, polls, calls, forwarding, and admin commands (/help,
/reboot, /kill, /donate) from Telegram. Includes one-command install
(install.sh) and interactive first-run setup (setup.sh) for a bare Ubuntu
server.