63 KiB
arch-01. Общая архитектура системы
Термины — в
arch-00-glossary.md. Приоритет документов — вREADME.md. Безопасность размещения на VM, OS-роли, SSH, секреты и production-деплой — вarch-06-service-hosting-security.md.
Назначение
HAN Chat - приложение для мигрантов, где стартовый экран знакомит клиента с сервисом и предлагает задать вопрос. Авторизация не требуется при первом входе: она запрашивается при попытке отправить первое сообщение, потому что в переписке могут обрабатываться персональные данные.
Зафиксированные решения MVP
- Авторизация: только OTP по номеру телефона (email-канал в MVP не используется).
- Вторая сторона чата: Битрикс24 Open Lines.
- Master source auth-данных: Keycloak; профиль в UI — кэш App DB с двусторонней sync через
bitrix-sync. - Диалог приложения соответствует диалогу в Битрикс24 Open Lines.
- Лиды и сделки в MVP не используются.
- Файлы production-хранилища: Selectel S3, бакет S3-data (логическое имя; физически два бакета —
han-chat-attachmentsдля файлов чата иhan-chat-documentsдля документов компании). - Файлы до проверки: Selectel S3, бакет S3-quarantine; после
200 allow— перенос в S3-data (attachments). Имена бакетов —arch-00-glossary.md; права доступа — ниже и в «Принципы безопасности». - Мультиязычность в первом релизе не нужна, но тексты должны храниться по мнемоникам для будущих переводов.
- Среда на первом этапе одна и проектируется как боевая.
- Вложения чата MVP: только изображения и PDF — см.
arch-04-settings-and-content.md, «Разрешённые типы файлов чата». - SMS OTP вводится поэтапно: до production rollout действует явный mock (
KEYCLOAK_OTP_MOCK_ENABLED=true); целевой real mode — Keycloak генерирует/локально проверяет OTP и создаёт durable order вsms-service, а worker асинхронно вызывает i-Digital Direct. Контракт и gates —module-11-idgtl-sms.md. - Популярный вопрос при выборе автоматически отправляется как сообщение; если пользователь не авторизован — сначала согласия и OTP, затем отправка.
- Notification Center v1 использует два контура: G — общие read-only гостевые кампании, P — персональные уведомления с состоянием в App DB. Виды, CTA, кнопки и палитра задаются каталогом данных.
- Инструкция
install_appвсегда открывается во внешней новой вкладке; iframe/модалка для неё не используется. - Перечень таблиц и миграций схемы
han_appпроектируетmodule-01-api-backendи его migration owner; владельцы остальных сервисов проектируют свои схемы. Arch фиксирует только разделение схем PostgreSQL и контракты между сервисами.
Пользовательские сценарии
- Клиент открывает мобильное или web-приложение и видит главный экран с приветствием, популярными вопросами и полем ввода.
- Frontend определяет, нужна ли новая UX-сессия, но до JWT не вызывает backend write-endpoint: клиент может изучить сервис без авторизации через guest UI и
GET /api/v1/public/*. - Если у клиента сохранён действующий refresh token, frontend выполняет silent refresh без OTP (см. «Поток возврата пользователя»).
- Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и инициирует отправку сообщения (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет.
- Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»).
- После успешной авторизации frontend с JWT вызывает
POST /api/v1/auth/bootstrap(в теле — принятые согласия): api-backend создаёт или находит локального пользователя поkeycloak_sub, сохраняет согласия наuser_id, создаёт или обновляет профиль; триггер App DB ставит задачу вsync_queueдляbitrix-sync. Затем, если у frontend нет активной UX-сессии или она истекла, вызываетсяPOST /api/v1/analytics/session-start. - api-backend выполняет find-or-create диалога (см. «Создание диалога») и отправляет сообщение (текст популярного вопроса или введённый клиентом).
- Сообщение клиента проходит Message Safety и через Bitrix24 Local App направляется в Битрикс24 Open Lines.
- Ответ оператора из Битрикс24 Open Lines поступает через Bitrix24 Local App в api-backend и отображается в чате приложения.
- Клиент может открыть историю диалогов.
- Клиент может открыть профиль, где данные структурированы блоками: «Личные данные» и «Документы». В дальнейшем могут добавляться новые блоки.
- Редактирование профиля из профиля недоступно. Для изменения данных клиент переходит в чат и пишет запрос оператору.
Компоненты верхнего уровня
- Expo App: единая frontend-кодовая база для iOS, Android и web.
- Keycloak: identity provider, OTP-only авторизация по номеру телефона.
- SMS Service: internal durable order API, шаблоны и бессрочный журнал SMS; отдельный worker вызывает i-Digital Direct, callback обновляет только журнал.
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
- Notification producers: сервисы приватной сети, создающие/отменяющие персональные уведомления через Internal API с отдельным Bearer token на
source;producer_testиспользуется только для smoke API. - Nginx Reverse Proxy: независимые точки входа ВМ1 и ВМ2; ВМ1 обслуживает приложение/Open Lines, ВМ2 — CRM webhook
bitrix-syncи private Message Safety API. - Message Safety Service: отдельный сервис ВМ2 проверки исходящих сообщений; target v2 →
200 allow|403 deny|202 pending+Location. - Bitrix24 Local App: локальное приложение, custom connector
han_mobile_appдля Bitrix24 Open Lines: чат, OAuth, webhook-события, маппингdialog_id↔bitrix_chat_id. - Bitrix24 sync service: двусторонняя синхронизация App DB ↔ Битрикс24 CRM (Contact на MVP; маппинг ID, очередь через триггеры, webhook от роботов Bitrix24).
- Managed PostgreSQL (приватная сеть, одна база): схемы
han_app,bitrix_sync,bitrix_local,keycloak,message_safety,sms— отдельный DB-user на схему. - Redis: rate limits API и realtime/service coordination (не OTP counters — они в Keycloak/SPI);
- S3-data: production-хранилище проверенных файлов чата (
han-chat-attachments) и документов компании (han-chat-documents). - S3-quarantine: временное хранилище загруженных файлов до вердикта Message Safety Service (
han-chat-quarantine); read-only дляmessage-safety. - observability: JSON-логи в stdout,
request_id,trace_id,ux_session_id(если передан), базовая трассировка через OpenTelemetry Collector.
Инфраструктура развёртывания (зафиксировано)
Production-like backend разделён на два контура в одной private network/VPC:
- ВМ1 HAN Chat: edge
nginx,api-backend,keycloak,sms-service/worker,bitrix-local-app, Redis DB0/DB1 и локальныйotel-collector; - ВМ2 Processing: в root Compose — собственный public/private
nginx,message-safetyAPI/worker,bitrix-sync, отдельный Redis Safety и локальныйotel-collector; на host — KESL 12.4 standalone и root-owned fail-closed broker с/run/han-kesl/scan.sock; - каждая VM имеет один root Compose project и отдельный root-owned systemd deployment unit;
- ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress на своих nginx; ВМ2 публикует только exact CRM webhook;
- ВМ1 вызывает ВМ2 по private HTTPS с проверкой internal CA, service token, cloud SG и host firewall;
- Битрикс24 вызывает public nginx ВМ2 напрямую; CRM webhook не проходит через ВМ1 и не создаёт на ней трафик/зависимость.
ВМ2 является независимым контуром вспомогательных сервисов. При её недоступности отправка пользовательских сообщений и CRM sync приостанавливаются, но чтение истории, auth, realtime и приём сообщений оператора на ВМ1 продолжаются. Недоступность ВМ1 не мешает ВМ2 принимать CRM webhook и выполнять накопленные workflows. Fail-open для Message Safety запрещён.
Базы данных — managed PostgreSQL того же провайдера в том же облачном кластере/VPC, без публичного доступа из интернета. VM подключается к БД только по приватной сети.
Размещение нескольких сервисов на одной VM не делает их одним доверенным контуром. Для каждого контейнера сохраняются least privilege, отдельные секреты, минимальные Docker networks и запрет доступа к Docker socket/host root. Обязательный baseline VM и контейнеров — arch-06-service-hosting-security.md.
Схема данных в managed PostgreSQL (перечень таблиц внутри схем — в модульных спецификациях, не в arch-*):
| База / схема | Сервисы | Назначение схемы |
|---|---|---|
одна база / han_app |
api-backend, bitrix-sync (ограниченный GRANT) |
прикладные данные приложения, очередь sync, audit |
одна база / bitrix_sync |
bitrix-sync |
worker state, retry/dead letter, sync audit |
одна база / message_safety |
message-safety |
verdict caches, safety tasks/audit, immutable versioned runtime config |
одна база / bitrix_local |
bitrix-local-app |
OAuth, inbox, dialog_sessions |
одна база / keycloak |
Keycloak | учётные записи, realm, сессии IdP |
одна база / sms |
sms-service, sms-worker |
шаблоны, runtime settings, бессрочный журнал отправки/доставки SMS |
Redis разделён по deployment boundary: DB0/DB1 остаются на ВМ1, отдельный Redis Safety находится на ВМ2. Оба являются ephemeral/coordination слоями; PostgreSQL остаётся durable source of truth. Selectel S3 — внешнее object storage: три бакета (han-chat-quarantine, han-chat-attachments, han-chat-documents); см. arch-00-glossary.md.
flowchart LR
client[Client] --> edge[VM1_EdgeNginx]
edge --> api[VM1_ApiBackend]
bitrix[Bitrix24] -->|"CRM webhook HTTPS"| publicGateway[VM2_PublicNginx]
publicGateway --> sync[BitrixSync]
api -->|"HTTPS 8443 + service token"| privateGateway[VM2_PrivateListener]
privateGateway --> safety[MessageSafetyApi]
safety --> worker[SafetyWorker]
worker -->|"Unix socket /run/han-kesl/scan.sock"| keslBroker[KESLBroker]
keslBroker --> hostKesl[HostKESL12_4]
worker --> s3q[S3Quarantine]
worker --> pg[ManagedPostgreSQL]
sync --> pg
sync --> bitrix
collector[VM2_OtelCollector] --> signoz[PrivateSigNoz]
Контекстная схема
flowchart LR
Client[Expo Mobile/Web App]
Nginx[Nginx Reverse Proxy]
Keycloak[Keycloak OTP]
SMS[SMS Service]
SMSWorker[SMS Worker]
Direct[i-Digital Direct]
API[Python api-backend]
Safety[Message Safety Service]
DB[(PostgreSQL)]
Redis[(Redis)]
Sync[Bitrix24 sync service]
LocalApp[Bitrix24 Local App]
Bitrix[Bitrix24 CRM]
S3Data[(S3-data: attachments + documents)]
S3Q[(S3-quarantine)]
Obs[observability]
Client -->|HTTPS REST + Realtime| Nginx
Nginx -->|/auth| Keycloak
Nginx -->|exact POST /callbacks/idgtl/sms| SMS
Nginx -->|"/api REST + WS realtime"| API
Keycloak --> DB
Keycloak -->|durable SMS order| SMS
SMS --> DB
SMSWorker --> DB
SMSWorker -->|HTTPS POST /api/v1/message| Direct
Direct -->|delivery callback| Nginx
API --> DB
API --> Redis
Client -->|presigned PUT| S3Q
API -->|presign / HeadObject / move / delete| S3Q
API -->|promote chat files| S3Data
API -->|internal check message| Safety
Safety --> DB
Safety --> Redis
Safety -->|read scan| S3Q
API -->|send messages| LocalApp
API -->|App DB writes| DB
Sync -->|sync_queue + profile| DB
Sync -->|CRM Contact REST| Bitrix
Bitrix -->|robot webhook| Sync
Bitrix -->|ONIMCONNECTOR*| LocalApp
LocalApp -->|imconnector.send.messages/status| Bitrix
LocalApp -->|normalized inbox events| API
API -->|WebSocket or polling fallback| Client
API --> Obs
Safety --> Obs
Sync --> Obs
LocalApp --> Obs
LocalApp --> DB
Архитектурные границы
Frontend
Отвечает за:
- стартовый экран с приветствием, популярными вопросами, полем ввода, историей и профилем;
- гостевой режим до первого сообщения;
- показ pop-up с обязательными согласиями на обработку персональных данных (со ссылками на согласие и политику ПД) и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
- сбор данных устройства для передачи в backend;
- управление аналитической UX-сессией на клиенте: после получения JWT —
session_start, хранениеux_session_idиlast_activity_atтолько в памяти, заголовокX-Ux-Session-Idв JWT-запросах; - хранение access token и refresh token в безопасном хранилище после авторизации;
- жизненный цикл access token: проактивное обновление по расписанию (до истечения
exp) и обработка401отapi-backend(см. «Обновление access token (frontend)»); - при открытии приложения: проверку refresh token → silent refresh через Keycloak или OTP-flow при истечении refresh token;
- отображение входящих сообщений от оператора;
- загрузку файлов в чат:
init→ presigned PUT в S3 →complete(байты не через api-backend); - работу с текстовыми мнемониками;
- отправку
traceparent/correlation id в backend.
Frontend не должен:
- хранить бизнес-логику синхронизации с Битрикс24;
- принимать решения о доступе к чужим документам или диалогам;
- обращаться напрямую к Битрикс24, Selectel S3 или базе данных.
api-backend
Отвечает за:
- публичные настройки приложения для frontend;
- проверку JWT от Keycloak для защищенных операций; при истёкшем или невалидном access token —
401(refresh выполняет frontend, не backend); - локальную регистрацию пользователя приложения:
find-or-createUserIdentityпоkeycloak_sub, создание минимальногоClientProfileдля нового пользователя, обновлениеlast_login_atдля существующего (после OTP — см.POST /api/v1/auth/bootstrap); - приём события
session_start: записьUxSession, audit/analytics-событие; не используется для контроля доступа; - валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic
- хранение согласий пользователя в App DB (
user_id, nullableux_session_id,client_ip, версии документов) — только после JWT; при bootstrapux_session_id=NULLдопустим, потому что новая UX-сессия создаётся следующим запросом; - профиль, структурированный блоками;
- API чата, истории, файлов и документов;
- realtime-доставку входящих сообщений клиенту;
- отправку сообщений клиента в Open Lines через Bitrix24 Local App;
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
- хранение истории диалогов;
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры →
sync_queue→bitrix-sync, без участия api-backend); - выдачу presigned URL на загрузку в S3-quarantine, проверку объекта при
complete, promote/delete после вердикта; - вызов Message Safety v2 (
POST /internal/safety/v2/messages/check) и интерпретацию200 allow,403 deny,202 pending; - при
200: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24; - при
403: удаление файлов из quarantine, безопасный ответ клиенту; - при
202: api-backend синхронно поллитLocationдо финального200/403, terminal failed или timeout, затем promote/Bitrix или cleanup;- это ожидание в рамках одного клиентского HTTP-соединения, а не общая очередь: другие запросы обрабатываются параллельно (workers/async);
- решение «быстрая проверка / долгая» принимает только
message-safety; на api-backend нет очереди анализа сообщений;
- запись checkpoint в
safety_tasks(App DB) на время poll — для recovery при timeout/crash (I1); - circuit breaker + timeout budget на вызовы
message-safetyиbitrix-local-app(I2); - auth-aware rate limits для сообщений, пользовательских и сервисных операций;
- аудит пользовательских действий;
- публичный каталог/гостевые кампании, JWT API Notification Center и Internal Create/Cancel; дедупликацию по бессрочной паре
(source, external_id); - применение каталога уведомлений без ветвления по
notification_type, пользовательские действия, документы и событияnotification.created|updated|closed; - expire job и очистку upload drafts. При скрытии TTL задаёт
date_expiredтолько если оно отсутствует; существующая дата не меняется; - единые ошибки и валидацию входных данных.
Bitrix24 Local App
Отвечает за Open Lines (чат):
- регистрацию локального приложения Bitrix24;
- OAuth lifecycle Bitrix24 и хранение токенов портала;
- регистрацию и активацию custom connector
han_mobile_appдля открытой линии 8; - прием публичных событий Bitrix24
ONIMCONNECTOR*на/bitrix/handler; - нормализацию событий Open Lines в доменные события HAN;
- хранение
dialog_sessions: связкаexternal_chat_id(=dialog_idприложения) ↔bitrix_chat_id↔session_id; - хранение локального inbox до готовности API;
- internal API для api-backend:
POST /internal/openlines/v1/messages,GET /internal/openlines/v1/dialogs/{external_chat_id}; - forward нормализованных событий оператора в API (
BITRIX_API_FORWARD_URL); - вызовы
imconnector.send.messagesиimconnector.send.status.delivery.
Не отвечает за:
- CRM Contact mapping и синхронизацию прочих CRM-сущностей;
- сохранение сообщений и истории чата в App DB;
- realtime-доставку в Expo App;
- бизнес-логику профиля и документов.
Bitrix24 sync service
Отвечает за асинхронную двустороннюю синхронизацию данных между App DB и Битрикс24 CRM по контракту module-07-bitrix-sync.md:
- канонический mapping и его историю в
bitrix_sync.entity_external_mapping; App DB не хранит CRM Contact ID; - App DB → Bitrix24: durable workflow для
contact.map_or_create,contact.update,contact.deactivate; - исправление связи: audited административный запрос запускает
contact.rebind; прямойUPDATEmapping запрещён; - Bitrix24 → App DB: durable webhook inbox, coalescing и reconciliation; запись профиля с transaction-local GUC
han.sync_suppress; - mastership по полям: телефон — App/Keycloak,
NAME/citizenship/email — Битрикс24; - batch, общий portal rate limiter, leases/fencing, retry до 24 часов и technical DLQ;
- business conflicts через смарт-процесс Битрикс24, technical failures через SigNoz;
- прямой доступ к схеме
han_appи собственнойbitrix_sync.
Не отвечает за:
- hot path чата Open Lines;
- OAuth lifecycle локального приложения Bitrix24;
- создание
UserIdentity/ClientProfileв auth-flow; - хранение
dialog_sessions.
Keycloak
Отвечает за:
- OTP-only регистрацию и вход;
- OTP по номеру телефона; генерация и локальная проверка кода, challenge lifecycle, limits и verify audit — в Keycloak;
- в real mode — заказ в
sms-serviceпо закрытомуPOST /internal/sms/v1/send; Keycloak ждёт только200/202+sms_message_id, не вызывает Direct и не читает provider statuses; - продуктовые лимиты OTP (
otp.phone.*изapp_settings) через authenticator/SPI и settings bridgeapi-backend(см. arch-04); durable counters/challenges/events — в provider-owned таблицах schemakeycloak, не в Redis и не вapi-backend; - хранение учетных записей;
- выдачу и обновление токенов (access + refresh);
- настройку realm, clients, roles, policies;
- публикацию OIDC discovery и JWKS для проверки JWT.
Парольная авторизация, magic link и социальные логины не входят в MVP.
Взаимодействия (MVP):
| С кем | Направление | Назначение |
|---|---|---|
| Expo frontend | Frontend → Keycloak (/auth/* через nginx) |
OTP login (Authorization Code + PKCE), Refresh Token Grant, logout |
api-backend |
api-backend → Keycloak JWKS/discovery | Валидация access token (issuer, audience, подпись); не вызывает Admin API в hot path |
| Managed PostgreSQL | Keycloak → схема keycloak |
Пользователи IdP, сессии, realm |
sms-service |
Keycloak → sms-service (real mode) |
Durable order; service token, idempotency key и sms_message_id |
Confidential backend client Keycloak (client credentials) в MVP не обязателен: S2S между нашими сервисами идёт по service tokens, не через Keycloak. Client можно завести заранее в realm как optional для будущих admin/ops сценариев.
Nginx Reverse Proxy (целевая двух-VM топология)
Отвечает за:
- прием внешнего HTTPS-трафика;
- TLS termination;
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
- маршрутизацию
/api/*в api-backend (включаяWS /api/v1/realtime); - маршрутизацию
/auth/*или выделенного auth-домена в Keycloak; - на nginx ВМ1 — маршрутизацию только
/bitrix/handler,/bitrix/install,/bitrix/placementвbitrix-local-app; - на отдельном public nginx ВМ2 — маршрутизацию только exact
/bitrix/sync/webhook/contactи/bitrix/sync/webhook/alertвbitrix-sync; ВМ1 эти paths не проксирует; - маршрутизацию только exact
POST /callbacks/idgtl/smsвsms-serviceпо HTTPS, с allowlist актуального IP Direct и без логирования Basic Authorization; - защиту internal endpoint
bitrix-local-appчерез private network илиnginx allowlist; - отсутствие публичной маршрутизации к
message-safety:api-backendВМ1 вызывает private nginx ВМ2:8443по HTTPS с internal CA и service token; Docker DNS/HTTP допустим только внутри ВМ2 за gateway; - передачу
X-Forwarded-For,X-Forwarded-Proto,X-Forwarded-Host,X-Request-ID(если клиент не прислалX-Request-ID— nginx генерирует UUID и прокидывает upstream); - базовые лимиты размера запроса и timeout;
- грубые edge rate limits по IP, route и зоне риска;
- TLS 1.2/1.3, HSTS, security headers и скрытие технологических заголовков;
- кэширование публичных endpoint настроек и контента;
- запрет доступа к внутренним сервисам и техническим портам извне.
Message Safety Service
Отвечает за:
- проверку входящих сообщений от пользователя (текст, ссылки, файлы);
- внутреннюю orchestration: синхронно текст и ссылки; при необходимости — async-проверка файлов;
- HTTP-контракт для api-backend:
200— синхронная проверка завершена, allow;403— синхронная проверка завершена, deny;202+task_id/Location— нужна async-проверка;
- task GET:
200 allow|403 deny|202 pending| terminal failed503; - SHA-256 хеширование и lookup кэша вердиктов;
- отдельный pipeline проверки ссылок;
- запись verdict caches,
safety_task, audit и immutableconfig_versionsв схемеmessage_safety; runtime role не активирует config; - target internal API:
POST /internal/safety/v2/messages/check,GET /internal/safety/v2/messages/tasks/{task_id}.
Не отвечает за:
- загрузку файлов клиентом, presigned URL, перемещение quarantine → S3-data, удаление из quarantine;
- сохранение сообщений, истории диалогов (CRM sync — зона
bitrix-sync, не api-backend); - доставку в Bitrix24 Open Lines и realtime клиенту;
- проверку JWT, согласий, edge rate limits;
- polling
task_idна стороне клиента запрещён — только api-backend, и только внутри обработкиPOST .../messages(sync wait до финального вердикта).
api-backend не решает, sync или async нужна проверка внутри Message Safety: это определяет Message Safety Service. Но для клиента POST .../messages всегда завершается финальным allow/deny (или ошибкой timeout/зависимости).
Гостевой режим (до JWT)
До OTP frontend работает локально без записи согласий и UX-сессии в App DB:
- UI главного экрана, популярные вопросы и публичный контент — через
GET /api/v1/public/*(без JWT); - pop-up согласий показывается до OTP, но факт принятия хранится только на клиенте до получения tokens;
POST /api/v1/consents,POST /api/v1/analytics/session-startи остальные write/API чата — только с JWT;- опциональный локальный
guest_session_id(UUID в secure storage) может использоваться frontend для своей аналитики/идемпотентности UI, но не является auth и не открывает backend write-endpoint.
Аналитическая UX-сессия (ux_session_id)
UX-сессия — период непрерывной активности авторизованного пользователя в приложении для аналитики и сквозной трассировки. Это не сессия Keycloak, не refresh/access token и не механизм авторизации.
Роли компонентов
Frontend (источник истины по правилам сессии):
- хранит
ux_session_idиlast_activity_atтолько в памяти (не в localStorage/secure storage); - вызывает
POST /api/v1/analytics/session-startтолько при наличии JWT (после OTP или silent refresh); - в гостевом режиме
session-startне вызывается; - при новой сессии сохраняет полученный
ux_session_id; - обновляет
last_activity_atпри пользовательской активности и при возврате из фона; - при resume проверяет
(now - last_activity_at) > idle_timeout→ при превышении — новая сессия (снова с JWT); - передаёт
X-Ux-Session-Idво всех JWT-запросах к backend, пока сессия активна.
api-backend:
- принимает
session_startтолько с валидным JWT, создаёт записьUxSessionсuser_id, возвращаетux_session_id; - пишет analytics/audit-событие
session_start(без PII); - включает
ux_session_idиз заголовка в JSON-логи (если передан); - отсутствие
ux_session_idне блокирует API (кроме endpoint, где id обязателен по контракту) — это не auth, но самsession-startбез JWT недоступен.
request_id — один HTTP-запрос; ux_session_id — период UX-активности для аналитики и корреляции логов.
Поток возврата пользователя (без OTP)
- Клиент открывает приложение. Пока нет JWT — гостевой UI;
session-startне вызывается. - Frontend проверяет наличие refresh token в secure storage.
- Если refresh token действителен — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), OTP не показывается, затем при необходимости начала новой UX-сессии вызывает
POST /api/v1/analytics/session-start. - Frontend работает как авторизованный пользователь (история, профиль, чат).
- Если refresh token отсутствует или истёк — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
Обновление access token (frontend)
Пока refresh token действителен, frontend сам поддерживает актуальный access token — не полагаясь только на открытие приложения и не дожидаясь истечения refresh token (использует его для обновления access token заранее).
Проактивное обновление по расписанию
- После получения tokens (OTP или refresh) frontend сохраняет access token, refresh token и момент истечения access token (
expиз JWT илиexpires_inиз ответа Keycloak). - Запускает таймер/scheduler: обновить access token до наступления
exp(рекомендуемый запас — 60 с доexp; константа модуля frontend). - По срабатыванию таймера — Refresh Token Grant к Keycloak, сохранение новой пары tokens, перепланирование следующего обновления.
- Single-flight: параллельные refresh-запросы не дублируются (один in-flight refresh, остальные ждут результат).
- Успешный refresh access token не создаёт новую UX-сессию и не вызывает
session_start.
Обработка 401 от api-backend
Если запрос с access token вернул 401 (токен уже истёк или отклонён):
- HTTP-клиент frontend один раз инициирует Refresh Token Grant (если refresh ещё не выполняется — через тот же single-flight).
- При успехе — подставляет новый access token и повторяет исходный запрос (без бесконечных retry).
- При неудаче refresh (
invalid_grant, истёк refresh token, ошибка Keycloak) — очищает tokens, переводит UI в гостевой режим; повторная авторизация — через OTP при следующем защищённом действии. - Запросы, пришедшие во время in-flight refresh, ставятся в очередь и выполняются после успешного обновления (или отклоняются при провале refresh).
- Тот же принцип — для WebSocket
/api/v1/realtime: при ошибке auth — refresh и переподключение с новым access token.
Разделение ответственности
| Компонент | Поведение |
|---|---|
| Frontend | scheduler refresh, intercept 401, retry, single-flight, хранение tokens |
| Keycloak | выдача и ротация tokens (Refresh Token Grant) |
| api-backend | проверка JWT; при невалидном/expired access token — 401, refresh не выполняет |
Создание диалога (MVP)
- У пользователя не более одного активного диалога: статус
open|waiting_for_company|waiting_for_client. Закрытые (closed) остаются в истории. POST /api/v1/dialogs: если активный диалог уже есть — возвращает его (200/ idempotent), новый не создаёт; иначе создаёт (201,status=open).- Диалог создаётся лениво при первой отправке сообщения авторизованным клиентом.
- Frontend перед
POST .../messagesвызываетPOST /api/v1/dialogs(заголовокIdempotency-Key), получаетdialog_idи использует его далее. - Популярный вопрос: после auth тот же порядок —
POST /dialogs→POST .../messagesс текстом вопроса. dialog_id=external_chat_idдля Open Lines (см.arch-00-glossary.md, «Идентификаторы»).- При первой доставке в Bitrix24
bitrix-local-appсоздаёт записьdialog_sessions. - Новый активный диалог после
closed— снова черезPOST /dialogs(когда продукт это разрешит; MVP: один активный в любой момент).
Поток авторизации (OTP)
Срабатывает, когда клиент ещё не имеет действующего refresh token (первый вход) или refresh token истёк. Если refresh token валиден — см. «Поток возврата пользователя».
- Клиент в гостевом режиме (только UI +
GET /api/v1/public/*). - Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
- Frontend показывает pop-up с тремя согласиями; факт принятия хранится локально до OTP.
- Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение.
- Клиент может опционально согласиться на рекламные коммуникации.
- Если обязательные согласия не даны, отправка блокируется.
- Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
- Keycloak запускает OTP-flow: в mock mode challenge сразу активен без SMS; в real mode Keycloak создаёт
ordering, генерирует OTP, заказывает SMS вsms-serviceи активирует challenge только после durable order. - Лимиты OTP на edge —
nginx; продуктовыеotp.phone.*применяет Keycloak. HTTP retry одного durable order использует прежние challenge/idempotency key и не увеличивает send counter. - Клиент вводит OTP и отправляет его в Keycloak.
- Keycloak проверяет корректность введённого OTP:
- при
KEYCLOAK_OTP_MOCK_ENABLED=true(MVP и любой режим с включённой заглушкой): введённое значение должно совпадать сKEYCLOAK_OTP_MOCK_CODEиз.env; - при
KEYCLOAK_OTP_MOCK_ENABLED=false: значение сверяется локально с HMAC OTP, сгенерированного Keycloak и переданного в закрытом заказеsms-service; статусы Direct и callback на verify не влияют. - при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 12 не выполняется.
- при
- При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
- Frontend с JWT вызывает
POST /api/v1/auth/bootstrap— в теле передаёт локально принятые согласия и device metadata (см. arch-02). api-backend атомарно:find-or-createпо JWTsub(keycloak_sub), телефон из JWT claims (не из body) → сохранениеUserConsentнаuser_idс nullableux_session_id(на bootstrap обычноNULL) → минимальный профиль. - Frontend вызывает
POST /api/v1/analytics/session-start(если нужна новая UX-сессия) и далее работает сX-Ux-Session-Id. - Триггер App DB ставит задачу
contact.map_or_createвsync_queue;bitrix-syncасинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM. - Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
Отдельный POST /api/v1/consents после первого входа нужен, когда пользователь заново принимает обновлённые версии документов (не часть OTP-flow).
Поток работы с чатом: клиент -> Битрикс24
В MVP сообщение клиента — content_kind text или file, не оба (см. arch-02-api-contracts.md, «Формат исходящего сообщения»).
Текстовое сообщение:
- Frontend вызывает
POST /api/v1/dialogs(еслиdialog_idещё нет), затем отправляетPOST /api/v1/dialogs/{dialog_id}/messagesс непустымtext(без вложения). - Nginx и API применяют rate limits.
- API вызывает Message Safety v2 (
POST /internal/safety/v2/messages/check) — шаги text/local links. - Далее — общая ветка вердикта (п. 5–8 ниже).
Файловое сообщение:
- Frontend инициализирует одно вложение (
POST .../attachments/init), получает presigned PUT в S3-quarantine, загружает байты напрямую в S3, затем вызываетPOST .../attachments/{attachment_id}/complete. - Frontend отправляет
POST /api/v1/dialogs/{dialog_id}/messagesсattachment_idиchecksum(полеtextпустое). - Nginx и API применяют rate limits.
- API синхронно вызывает Message Safety Service — шаг проверки файла (текст и ссылки пропускаются, если
textпуст).
Общая ветка вердикта (оба типа):
403 deny: API удаляет quarantine (если был файл), выставляетsafety_status=blocked,delivery_status=rejected, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.200 allow: API переносит файл в S3-data attachments (если был), сохраняет сообщение (safety_status=allowed,delivery_status=accepted) и фиксирует задачу доставки в Open Lines. После успешной отправки черезbitrix-local-appстатус становитсяdelivery_status=delivered, API подтверждает клиенту финальный результат;Dialog.status→waiting_for_company. Если Bitrix24/S3/dependency недоступны после allow, статус становитсяdelivery_status=failed, клиент получает безопасную ошибку зависимости.202 pending: api-backend пишет checkpoint сLocationи синхронно поллит его сRetry-After, пока не получит финальный вердикт/terminal failure или не истечёт budget. Public POST остаётся открытым; другие запросы не блокируются.- финальный
200 allow→ как п. 6, затем ответ клиенту; - финальный
403 deny→ как п. 5, затем ответ клиенту; - timeout / недоступность safety →
delivery_status=failed, безопасная ошибка клиенту (503/504), quarantine не promote; recovery поsafety_tasks— зона модуля.
- финальный
Клиент на POST .../messages получает только финальный результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
Надёжность доставки и recovery
- Для доставки в Bitrix24 используется transactional outbox/checkpoint в App DB: запись
Messageи запись намерения доставки фиксируются атомарно, а повторная отправка вbitrix-local-appидемпотентна поmessage_id/Idempotency-Key. delivery_status=acceptedозначает, что API принял сообщение и завершил safety allow, но ещё не получил подтверждение доставки в Open Lines.delivery_status=deliveredвыставляется только после успешного ответаbitrix-local-appо приёме сообщения для Bitrix24 Open Lines.- Recovery по
han_app.safety_tasksвосстанавливает сценарии202 pendingпосле timeout/crash, опрашивает сохранённыйLocation, затем идемпотентно выполняет conditional promote/delete и обновляет App DB. - Объекты в S3-quarantine не удаляются при timeout safety до финального verdict; orphan-cleanup удаляет только просроченные объекты без активного
safety_tasksили attachment metadata.
Поток работы с чатом: Битрикс24 -> клиент
- Оператор отвечает клиенту в Битрикс24 Open Lines.
- Битрикс24 отправляет
ONIMCONNECTOR*webhook/event вbitrix-local-app. bitrix-local-appпроверяетapplication_token, нормализует payload и сохраняет idempotent inbox.bitrix-local-appобогащает событие данными изdialog_sessionsи forward-ит в API, еслиBITRIX_API_FORWARD_URLвключен. При недоступности API событие остаётся во внутреннем inbox, повторяется с backoff и после исчерпания retry попадает в DLQ; дубликаты определяются по(external_chat_id, bitrix_message_id).- api-backend находит локальный диалог по
external_chat_id(=dialog_id, см.arch-00-glossary.md). - api-backend сохраняет входящее сообщение в App DB (
sender_type=company,delivery_status=delivered). Файл оператора (если есть) — в бакет S3-data attachments (han-chat-attachments) с metadata вMessageAttachment; бакет documents зарезервирован для документов компании в профиле (post-MVP). По факту сообщения API обновляетDialog.status: входящее от оператора →waiting_for_client, исходящее от клиента →waiting_for_company(значения — arch-00). bitrix-local-appподтверждает доставку в Bitrix24 черезimconnector.send.status.deliveryпосле успешного сохранения события в api-backend или идемпотентного duplicate-ack.- api-backend публикует событие для frontend через WebSocket (
WS /api/v1/realtime). Если realtime недоступен, frontend получает сообщение через pollingGET /api/v1/dialogs/{dialog_id}/messages?after=.... - Frontend отображает сообщение оператора в чате.
- При получении от
bitrix-local-appдоменного событияdialog.closed(Bitrix24ONIMCONNECTORDIALOGFINISH) API переводитDialog.statusвclosed.
Документы компании
Notification Center v1 регистрирует переданные продюсером объекты han-chat-documents в реестре documents и связывает их с уведомлением. Это первый действующий канал наполнения будущего общего блока профиля; доставка из Bitrix24 остаётся вне scope.
Скачивание выполняется owner-only по короткому presigned GET с audit. Для вида с hide_on_document_download=true первое скачивание любого связанного документа атомарно скрывает уведомление; последующие скачивания не меняют состояние. Если date_expired уже задано, оно сохраняется; TTL скрытия устанавливает дату только при её отсутствии.
Профиль клиента
Профиль должен быть блочным.
Блок "Личные данные":
- ФИО;
- гражданство;
- номер телефона в РФ;
- зарубежный номер телефона;
- email.
Блок "Документы":
- перечень документов, отправленных клиенту компанией (в MVP — пустой до реализации бэклога);
- дата отправки;
- наименование документа;
- возможность скачать документ (после реализации доставки).
Редактирование данных профиля недоступно.
Sync профиля и master для PII
App DB — локальный кэш для UI. Двусторонний sync — bitrix-sync (имена полей — arch-00-glossary.md):
- Auth-телефон: master — Keycloak (
UserIdentity.phone_number); только его фактическое изменение инициируетcontact.update. - ФИО, гражданство, email: master — Битрикс24; App хранит последний успешно полученный snapshot для UI и не отправляет эти поля обратно.
- App → Bitrix: триггеры
han_app→sync_queue(contact.map_or_create,contact.update,contact.deactivate). - Bitrix → App: durable webhook inbox + reconciliation; запись с
SET LOCAL han.sync_suppress='true'без эхо. - Конфликт: универсального правила «последнее событие побеждает» нет; применяется field mastership. Несовпадение identity/mapping создаёт business alert и не перезаписывает профиль.
Аудит скачиваний
При выдаче presigned URL на скачивание вложений чата (GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url) и документов профиля (GET /api/v1/documents/{document_id}/download-url) api-backend пишет audit-событие в App DB:
| Поле | Значение |
|---|---|
event_type |
attachment.download_url_issued / document.download_url_issued |
user_id |
текущий пользователь из JWT |
resource_type |
attachment / document |
resource_id |
UUID сущности |
ux_session_id |
из заголовка X-Ux-Session-Id |
request_id |
из заголовка запроса |
ip, user_agent |
из proxy headers |
В audit не сохраняются presigned URL, содержимое файлов и PII. Формат таблицы — в модуле database.
Realtime (кратко)
Детальный контракт — arch-02-api-contracts.md, раздел «Realtime».
- Transport: только WebSocket
WS /api/v1/realtime(JWT). SSE в MVP не используется. - Путь входит в
/api/*; отдельный location/realtime/*в nginx не нужен. - Fallback: polling
GET /api/v1/dialogs/{dialog_id}/messages?after=.... - События: новое сообщение, смена
delivery_status/safety_status, сменаDialog.status. - Подписка расширена опциональным
notifications(defaultfalse); канал пользователя передаётnotification.created,notification.updated,notification.closed, включая эхо инициатору. После reconnect источник истины — REST.
Принципы безопасности
- Все защищенные пользовательские API требуют валидный JWT.
- Компрометация одного сервиса не должна автоматически давать host root, Docker daemon, секреты или сетевой доступ соседних сервисов; требования к VM и production-деплою — в
arch-06-service-hosting-security.md. - Без JWT доступны только read-only публичные endpoint:
GET /api/v1/public/*(rate limit + CORS + кэш). Write-endpoint (consents,session-start, чат, профиль и т.д.) требуют JWT. - Все внешние пользовательские соединения работают через HTTPS.
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
- TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети.
- TLS 1.0/1.1 и слабые шифры запрещены.
- HSTS обязателен после проверки домена и сертификата.
- INPUT-validation на api-backend
- использовать только Параметризованные SQL-запросы
- обязательное Экранирование вывода
- настройка CORS только на разрешённые домены (
security.cors.allowed_originsвapp_settings, см. arch-04) - настройка Secure Headers (CSP, X-Frame-Options и др.)
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим
user_id. - Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос.
- Все публичные id создаются в формате UUID.
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (канонический контракт —
arch-02-api-contracts.md, «Service tokens (internal API)»; значения переменных —arch-04-settings-and-content.md). - Rate limits применяются минимум на двух уровнях: edge-лимиты в
nginxи пользовательские лимиты в API с состоянием в Redis. - Исходящие сообщения пользователя: internal
POST /internal/safety/v2/messages/check→ при202api-backend синхронно поллитLocationдо финального200/403, terminal failed503или timeout; public API не становится async. - Файлы пользователя до финального
allowтолько в S3-quarantine; в S3-data — после200 allow. - У клиента нет постоянных S3 credentials. Загрузка — presigned PUT в S3-quarantine, выданный
api-backend; скачивание — presigned GET. Байты файла не проксируются черезapi-backend. message-safety— read-only к S3-quarantine, без прав записи в бакеты.- Вызовы
message-safetyиbitrix-local-appзащищены timeout budget и circuit breaker (см. arch-04). - Все изменяемые параметры, телефоны, лимиты, mime types и флаги хранятся в настройках (
arch-04-settings-and-content.md). - PII-данные не пишутся в логи в открытом виде.
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.
Backend-репозиторий и инфраструктура
Состав backend-контура
Минимальный целевой real-SMS контур разделён на два stack:
- ВМ1:
nginx,api-backend,keycloak,sms-service/worker,bitrix-local-app, Redis DB0/DB1,otel-collector; - ВМ2: nginx с public webhook/private internal server blocks,
message-safetyAPI/worker,bitrix-sync, Redis Safety,otel-collectorв Compose; KESL 12.4 standalone и root-owned broker на host.clamd/freshclamудалены из Compose.
До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
Предлагаемая структура backend-репозитория
Каноническая структура root Compose, service includes, networks и mounts задаётся в arch-03-docker-compose-blueprint.md. Детальная внутренняя структура сервиса определяется его профильной спецификацией.
Compose-контуры
backend/docker-compose.yml является единственным root Compose ВМ1; processing/docker-compose.yml — единственным root Compose ВМ2. Оба используют include и отдельные root-owned systemd units. Cross-host Docker network не используется.
На каждой VM host ports публикует только её nginx. ВМ1 публикует 80/443 своего application host. ВМ2 публикует 80/443 отдельного webhook host и private 8443; public server block ВМ2 допускает только exact CRM webhook, private listener доступен только SG ВМ1/ops.