telegram/client: Bot API на fetch с таймаутом 5 с; ошибки делятся на
повторяемые (сеть, таймаут, 429, 5xx) и конфигурационные (400/403…),
токен не попадает ни в тексты ошибок, ни в tg_last_error.
format: заявка и уточнение по шаблону плана, HTML с экранированием,
источник из utm / referrer, первый визит.
outbox: Notifier с отправкой после ответа и тиком раз в минуту, пауза
1, 2, 4… мин, уточнение только после своей заявки, после 10 попыток —
в «Систему». system: тема «Система» с троттлингом ×N за период.
bot: long polling, только TELEGRAM_ADMIN_ID, /setup создаёт темы и
присылает id в личку. config: всё окружение в одном месте.
rateLimit.ts: 5 запросов / 10 минут на IP в памяти, фиксированное окно,
чистка устаревших записей по ходу запросов, 429 с Retry-After. IP из
X-Real-IP только от loopback-сокета (плюс TRUSTED_PROXIES для шлюза
docker), иначе адрес сокета; в базу IP не пишем. Honeypot → 201 без
записи и уведомления. isPaused() → 503 на время восстановления базы.
Тесты на каждый код ответа из таблицы плана.
saveLead в одной IMMEDIATE-транзакции: заявка с тем же телефоном
моложе 10 минут → уточнение, если имя, интерес или комментарий
отличаются от последней версии, иначе повтор без записи. Окно от
created_at заявки, уточнения его не продлевают. Ответ 200 duplicate,
notifier получает id уточнения и список изменившихся полей.
db/: better-sqlite3 по DB_PATH (WAL, foreign_keys), миграции через
PRAGMA user_version, схема leads и lead_clarifications по плану.
createApp(deps) с инъекцией базы, часов и notifier (пока no-op, зовётся
после ответа). POST /api/lead: только JSON (415), тело до 16 КБ (413),
validateLead → 400, запись → 201. index.ts: env и graceful shutdown.