Grok version

This commit is contained in:
mi
2026-07-09 12:39:52 +03:00
parent ea8bb6181a
commit 8835677860
7 changed files with 353 additions and 155 deletions
+10 -1
View File
@@ -48,9 +48,18 @@
|---|---| |---|---|
| Доставка документов компании из Bitrix24 в приложение (`bitrix-sync``api-backend`, уведомление клиента) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9; arch-01 — заглушка UI «Документы» | | Доставка документов компании из Bitrix24 в приложение (`bitrix-sync``api-backend`, уведомление клиента) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9; arch-01 — заглушка UI «Документы» |
| Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 10 | | Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 10 |
| Изоляция `bitrix-sync` на отдельную VM | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 8 |
## Открытые пробелы
| # | Пробел | Статус |
|---|---|---|
| G8 | Явный список `is_public=true` для ключей `app_settings` | Отложить до оформления сервисов; seed в модуле `database` |
## Обновление документации ## Обновление документации
- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости). - Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости).
- Новый env или ключ `app_settings` → arch-04. - Новый env или ключ `app_settings` → arch-04.
- Новый термин → arch-00, затем поиск по arch-*. - Новый термин / enum → arch-00, затем поиск по arch-*.
- Закрытие пробела → убрать из «Открытые пробелы» и отразить решение в arch-*.
+37 -6
View File
@@ -22,12 +22,13 @@
|---|---|---| |---|---|---|
| `UserIdentity` | `han_app` | Локальный пользователь, связь с Keycloak | | `UserIdentity` | `han_app` | Локальный пользователь, связь с Keycloak |
| `UxSession` | `han_app` | Аналитическая UX-сессия (период активности пользователя) | | `UxSession` | `han_app` | Аналитическая UX-сессия (период активности пользователя) |
| `UserConsent` | `han_app` | Запись о принятии согласий (до OTP) | | `UserConsent` | `han_app` | Запись о принятии согласий (после OTP, привязка к `user_id`) |
| `ClientProfile` | `han_app` | Кэш профиля для UI | | `ClientProfile` | `han_app` | Кэш профиля для UI |
| `Dialog` | `han_app` | Диалог клиента с Open Lines | | `Dialog` | `han_app` | Диалог клиента с Open Lines |
| `Message` | `han_app` | Сообщение в диалоге | | `Message` | `han_app` | Сообщение в диалоге |
| `MessageAttachment` | `han_app` | Вложение к сообщению | | `MessageAttachment` | `han_app` | Вложение к сообщению |
| `sync_queue` | `han_app` | Очередь sync App → Bitrix24 | | `sync_queue` | `han_app` | Очередь sync App → Bitrix24 |
| `safety_tasks` | `han_app` | Checkpoint sync-wait Message Safety (`task_id`) для recovery |
| `entity_external_mapping` | `han_app` | Маппинг App entity ↔ Bitrix entity | | `entity_external_mapping` | `han_app` | Маппинг App entity ↔ Bitrix entity |
| `app_settings` | `han_app` | Бизнес-настройки | | `app_settings` | `han_app` | Бизнес-настройки |
| `text_resources` | `han_app` | Тексты UI по мнемоникам | | `text_resources` | `han_app` | Тексты UI по мнемоникам |
@@ -40,8 +41,9 @@
|---|---| |---|---|
| `dialog_id` | UUID диалога в приложении; **равен** `external_chat_id` в Open Lines | | `dialog_id` | UUID диалога в приложении; **равен** `external_chat_id` в Open Lines |
| `external_chat_id` | Идентификатор чата для `bitrix-local-app` / `imconnector` | | `external_chat_id` | Идентификатор чата для `bitrix-local-app` / `imconnector` |
| `keycloak_sub` | Subject JWT Keycloak; ключ `UserIdentity` | | `keycloak_sub` | Subject JWT Keycloak (`sub`); ключ `UserIdentity` |
| `guest_session_id` | UUID гостевой сессии до OTP | | `phone_number` | Auth-телефон пользователя; master — Keycloak; в App DB пишется из JWT claims при `bootstrap`, не из body клиента |
| `guest_session_id` | Опциональный локальный UUID на устройстве (UI); **не** auth и **не** открывает write API |
| `ux_session_id` | UUID **аналитической UX-сессии**; заголовок `X-Ux-Session-Id` | | `ux_session_id` | UUID **аналитической UX-сессии**; заголовок `X-Ux-Session-Id` |
| `bitrix_contact_id` | ID Contact в Bitrix24 CRM | | `bitrix_contact_id` | ID Contact в Bitrix24 CRM |
| `bitrix_chat_id` | ID чата Open Lines в Bitrix24 | | `bitrix_chat_id` | ID чата Open Lines в Bitrix24 |
@@ -60,10 +62,10 @@
- Идентификатор периода — **`ux_session_id`** (UUID). - Идентификатор периода — **`ux_session_id`** (UUID).
- Начало периода фиксируется событием **`session_start`** (**ровно один раз** на период). - Начало периода фиксируется событием **`session_start`** (**ровно один раз** на период).
- Запись создаётся в App DB при `POST /api/v1/analytics/session-start` (см. arch-02). - Запись создаётся в App DB при `POST /api/v1/analytics/session-start` (**только с JWT**, см. arch-02).
- Frontend передаёт **`X-Ux-Session-Id`** во всех запросах к backend, пока сессия активна. - Frontend передаёт **`X-Ux-Session-Id`** во всех JWT-запросах к backend, пока сессия активна.
**`UxSession` не является механизмом авторизации.** Отсутствие или неизвестный `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту, напр. `POST /api/v1/consents`). **`UxSession` не является механизмом авторизации.** Отсутствие или неизвестный `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту). Сам endpoint `session-start` без JWT недоступен.
### Когда начинается **новая** `UxSession` ### Когда начинается **новая** `UxSession`
Новый **`ux_session_id`** + событие **`session_start`** — **только** если: Новый **`ux_session_id`** + событие **`session_start`** — **только** если:
@@ -76,17 +78,41 @@
- успешный OTP или refresh access token; - успешный OTP или refresh access token;
- навигация между экранами внутри приложения. - навигация между экранами внутри приложения.
## `Dialog.status`
| Значение | Смысл |
|---|---|
| `open` | Диалог создан, сообщений ещё нет (сразу после `POST /dialogs`) |
| `waiting_for_company` | Последнее значимое событие — исходящее от клиента; ждём оператора |
| `waiting_for_client` | Последнее значимое событие — входящее от оператора; ждём клиента |
| `closed` | Диалог закрыт в Open Lines (`dialog.closed` / `ONIMCONNECTORDIALOGFINISH`) |
Переходы задаёт `api-backend` (см. arch-01, потоки чата). Значения — **строковые enum** в API и App DB (не sequence-справочник).
## `Message` — enum и поля ## `Message` — enum и поля
| Имя | Допустимые значения | | Имя | Допустимые значения |
|---|---| |---|---|
| `Message.sender_type` | `client`, `company` | | `Message.sender_type` | `client`, `company` |
| `Message.safety_status` | `pending`, `allowed`, `blocked` (`needs_review` — зарезервирован, MVP не используется) | | `Message.safety_status` | `pending`, `allowed`, `blocked` (`needs_review` — зарезервирован, MVP не используется) |
| `Message.delivery_status` | `accepted`, `processing`, `delivered`, `rejected`, `failed` |
| `Message.text` | текст сообщения; пустая строка для файлового сообщения | | `Message.text` | текст сообщения; пустая строка для файлового сообщения |
| `content_kind` (логическое) | `text`, `file` — тип исходящего сообщения клиента (MVP) | | `content_kind` (логическое) | `text`, `file` — тип исходящего сообщения клиента (MVP) |
Семантика `allow` / `deny` / `pending` в `message-safety` и HTTP-коды — [`arch-02-api-contracts.md`](arch-02-api-contracts.md). Семантика `allow` / `deny` / `pending` в `message-safety` и HTTP-коды — [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
### `Message.delivery_status` (смысл)
| Значение | Когда |
|---|---|
| `accepted` | Сообщение принято API, safety ещё не завершена или только начата |
| `processing` | Внутренний/transient на время sync-wait safety; клиенту на `POST .../messages` не отдаётся как финальный ответ |
| `delivered` | Финальный `allow`, сообщение ушло в Open Lines (или входящее от оператора сохранено) |
| `rejected` | Финальный `deny` от Message Safety |
| `failed` | Инфраструктурная ошибка доставки (Bitrix/S3), не safety-deny |
Realtime-событие `message.status` передаёт актуальные `safety_status` и/или `delivery_status`.
## `MessageAttachment.scan_status` ## `MessageAttachment.scan_status`
| Значение | Смысл | | Значение | Смысл |
@@ -96,6 +122,11 @@
| `infected` | Проверка завершена, deny | | `infected` | Проверка завершена, deny |
| `failed` | Ошибка инфраструктуры проверки | | `failed` | Ошибка инфраструктуры проверки |
## Строковые enum vs справочники
- **Строковые enum** (значения в API/контрактах): `Dialog.status`, `Message.sender_type`, `Message.safety_status`, `Message.delivery_status`, `MessageAttachment.scan_status`, `content_kind`, `start_reason`.
- **Справочники (sequence ID)** — для больших/изменяемых списков UI и доменных классификаторов (типы документов post-MVP, причины и т.п.); правило — [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md).
## Мнемоники internal API ## Мнемоники internal API
Префикс: **`/internal/{service_mnemonic}/v1/`**. Health: **`/health/*`**. Префикс: **`/internal/{service_mnemonic}/v1/`**. Health: **`/health/*`**.
+91 -65
View File
@@ -30,7 +30,7 @@ HAN Chat - приложение для мигрантов, где стартов
3. Если у клиента сохранён **действующий refresh token**, frontend выполняет silent refresh **без OTP** (см. «Поток возврата пользователя»). 3. Если у клиента сохранён **действующий refresh token**, frontend выполняет silent refresh **без OTP** (см. «Поток возврата пользователя»).
4. Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и **инициирует отправку сообщения** (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет. 4. Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и **инициирует отправку сообщения** (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет.
5. Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»). 5. Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»).
6. После успешной авторизации api-backend создаёт или находит локального пользователя по `keycloak_sub`, связывает ранее сохранённые согласия с `guest_session_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. 6. После успешной авторизации frontend с JWT вызывает **`POST /auth/bootstrap`** (в теле — принятые согласия): api-backend создаёт или находит локального пользователя по `keycloak_sub`, сохраняет согласия на `user_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. Затем при необходимости — `POST /analytics/session-start`.
7. api-backend выполняет find-or-create диалога (см. «Создание диалога») и отправляет сообщение (текст популярного вопроса или введённый клиентом). 7. api-backend выполняет find-or-create диалога (см. «Создание диалога») и отправляет сообщение (текст популярного вопроса или введённый клиентом).
8. Сообщение клиента проходит Message Safety и через Bitrix24 Local App направляется в Битрикс24 Open Lines. 8. Сообщение клиента проходит Message Safety и через Bitrix24 Local App направляется в Битрикс24 Open Lines.
9. Ответ оператора из Битрикс24 Open Lines поступает через Bitrix24 Local App в api-backend и отображается в чате приложения. 9. Ответ оператора из Битрикс24 Open Lines поступает через Bitrix24 Local App в api-backend и отображается в чате приложения.
@@ -44,11 +44,11 @@ HAN Chat - приложение для мигрантов, где стартов
- Keycloak: identity provider, OTP-only авторизация по номеру телефона. - Keycloak: identity provider, OTP-only авторизация по номеру телефона.
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой. - api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
- Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24. - Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24.
- Message Safety Service: отдельный сервис проверки входящих сообщений; синхронный вызов из API → `200 allow` | `403 deny` | `203 pending` + `task_id`. - Message Safety Service: отдельный сервис проверки входящих сообщений; вызов из API → `200 allow` | `403 deny` | `203 pending` + `task_id` (при `203` api-backend синхронно поллит task до финального вердикта, без очереди анализа на api-backend).
- Bitrix24 Local App: локальное приложение, custom connector `han_mobile_app` для Bitrix24 Open Lines: чат, OAuth, webhook-события, маппинг `dialog_id``bitrix_chat_id`. - 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). - Bitrix24 sync service: двусторонняя синхронизация App DB ↔ Битрикс24 CRM (Contact на MVP; маппинг ID, очередь через триггеры, webhook от роботов Bitrix24).
- Managed PostgreSQL (приватная сеть, одна база): схемы `han_app`, `bitrix_sync`, `bitrix_local`, `keycloak`, `message_safety` — отдельный DB-user на схему. - Managed PostgreSQL (приватная сеть, одна база): схемы `han_app`, `bitrix_sync`, `bitrix_local`, `keycloak`, `message_safety` — отдельный DB-user на схему.
- Redis: rate limits, временные счетчики OTP и realtime/service coordination. - Redis: rate limits API и realtime/service coordination (**не** OTP counters — они в Keycloak/SPI);
- S3-data: production-хранилище проверенных файлов чата (`han-chat-attachments`) и документов компании (`han-chat-documents`). - S3-data: production-хранилище проверенных файлов чата (`han-chat-attachments`) и документов компании (`han-chat-documents`).
- S3-quarantine: временное хранилище загруженных файлов до вердикта Message Safety Service (`han-chat-quarantine`); read-only для `message-safety`. - S3-quarantine: временное хранилище загруженных файлов до вердикта Message Safety Service (`han-chat-quarantine`); read-only для `message-safety`.
- observability: JSON-логи в stdout, `request_id`, `trace_id`, **`ux_session_id`** (если передан), базовая трассировка через OpenTelemetry Collector. - observability: JSON-логи в stdout, `request_id`, `trace_id`, **`ux_session_id`** (если передан), базовая трассировка через OpenTelemetry Collector.
@@ -95,12 +95,13 @@ flowchart LR
Client -->|HTTPS REST + Realtime| Nginx Client -->|HTTPS REST + Realtime| Nginx
Nginx -->|/auth| Keycloak Nginx -->|/auth| Keycloak
Nginx -->|/api + /realtime| API Nginx -->|/api (REST + WS realtime)| API
Keycloak --> DB Keycloak --> DB
API --> DB API --> DB
API --> Redis API --> Redis
API -->|upload / move / delete| S3Q Client -->|presigned PUT| S3Q
API -->|promote delivered files| S3Data API -->|presign / HeadObject / move / delete| S3Q
API -->|promote chat files| S3Data
API -->|internal check message| Safety API -->|internal check message| Safety
Safety --> DB Safety --> DB
Safety --> Redis Safety --> Redis
@@ -113,7 +114,7 @@ flowchart LR
Bitrix -->|ONIMCONNECTOR*| LocalApp Bitrix -->|ONIMCONNECTOR*| LocalApp
LocalApp -->|imconnector.send.messages/status| Bitrix LocalApp -->|imconnector.send.messages/status| Bitrix
LocalApp -->|normalized inbox events| API LocalApp -->|normalized inbox events| API
API -->|WebSocket/SSE or polling fallback| Client API -->|WebSocket or polling fallback| Client
API --> Obs API --> Obs
Safety --> Obs Safety --> Obs
Sync --> Obs Sync --> Obs
@@ -131,12 +132,12 @@ flowchart LR
- гостевой режим до первого сообщения; - гостевой режим до первого сообщения;
- показ pop-up с обязательными согласиями на обработку персональных данных и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации; - показ pop-up с обязательными согласиями на обработку персональных данных и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
- сбор данных устройства для передачи в backend; - сбор данных устройства для передачи в backend;
- **управление аналитической UX-сессией** на клиенте: определение начала нового периода активности, хранение `ux_session_id` и `last_activity_at` **только в памяти**, отправка `session_start`, заголовок `X-Ux-Session-Id` во всех запросах; - **управление аналитической UX-сессией** на клиенте: после получения JWT — `session_start`, хранение `ux_session_id` и `last_activity_at` **только в памяти**, заголовок `X-Ux-Session-Id` в JWT-запросах;
- хранение access token и refresh token в безопасном хранилище после авторизации; - хранение access token и refresh token в безопасном хранилище после авторизации;
- **жизненный цикл access token**: проактивное обновление по расписанию (до истечения `exp`) и обработка **`401`** от `api-backend` (см. «Обновление access token (frontend)»); - **жизненный цикл access token**: проактивное обновление по расписанию (до истечения `exp`) и обработка **`401`** от `api-backend` (см. «Обновление access token (frontend)»);
- при открытии приложения: проверку refresh token → silent refresh через Keycloak **или** OTP-flow при истечении refresh token; - при открытии приложения: проверку refresh token → silent refresh через Keycloak **или** OTP-flow при истечении refresh token;
- отображение входящих сообщений от оператора; - отображение входящих сообщений от оператора;
- загрузку файлов в чат через backend; - загрузку файлов в чат: `init` → presigned PUT в S3 → `complete` (байты не через api-backend);
- работу с текстовыми мнемониками; - работу с текстовыми мнемониками;
- отправку `traceparent`/correlation id в backend. - отправку `traceparent`/correlation id в backend.
@@ -155,7 +156,7 @@ Frontend не должен:
- локальную регистрацию пользователя приложения: `find-or-create` `UserIdentity` по `keycloak_sub`, создание минимального `ClientProfile` для нового пользователя, обновление `last_login_at` для существующего (после OTP — см. `POST /api/v1/auth/bootstrap`); - локальную регистрацию пользователя приложения: `find-or-create` `UserIdentity` по `keycloak_sub`, создание минимального `ClientProfile` для нового пользователя, обновление `last_login_at` для существующего (после OTP — см. `POST /api/v1/auth/bootstrap`);
- **приём события `session_start`**: запись `UxSession`, audit/analytics-событие; **не** используется для контроля доступа; - **приём события `session_start`**: запись `UxSession`, audit/analytics-событие; **не** используется для контроля доступа;
- валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic - валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic
- хранение согласий пользователя в App DB (`guest_session_id`, **`ux_session_id`**, **`client_ip`**, версии документов); - хранение согласий пользователя в App DB (**`user_id`**, **`ux_session_id`**, **`client_ip`**, версии документов) — только после JWT;
- профиль, структурированный блоками; - профиль, структурированный блоками;
- API чата, истории, файлов и документов; - API чата, истории, файлов и документов;
- realtime-доставку входящих сообщений клиенту; - realtime-доставку входящих сообщений клиенту;
@@ -163,11 +164,15 @@ Frontend не должен:
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App; - прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
- хранение истории диалогов; - хранение истории диалогов;
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue``bitrix-sync`, без участия api-backend); - запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue``bitrix-sync`, без участия api-backend);
- загрузку файлов из чата в S3-quarantine до проверки; - выдачу **presigned URL** на загрузку в S3-quarantine, проверку объекта при `complete`, promote/delete после вердикта;
- синхронный вызов Message Safety Service (`POST /internal/safety/v1/messages/check`) и интерпретацию ответа: `200 allow`, `403 deny`, `203 pending` + `task_id`; - синхронный вызов Message Safety Service (`POST /internal/safety/v1/messages/check`) и интерпретацию ответа: `200 allow`, `403 deny`, `203 pending` + `task_id`;
- при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24; - при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24;
- при `403`: удаление файлов из quarantine, безопасный ответ клиенту; - при `403`: удаление файлов из quarantine, безопасный ответ клиенту;
- при `203`: сохранение сообщения со статусом ожидания проверки, ответ клиенту «обрабатывается», опрос `GET /internal/safety/v1/messages/tasks/{task_id}` и доставка цепочки после финального `200` или cleanup после `403`; - при `203`: api-backend **синхронно поллит** `GET /internal/safety/v1/messages/tasks/{task_id}` до финального `200`/`403` (timeout budget — arch-04), затем 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 для сообщений, пользовательских и сервисных операций; - auth-aware rate limits для сообщений, пользовательских и сервисных операций;
- аудит пользовательских действий; - аудит пользовательских действий;
- единые ошибки и валидацию входных данных. - единые ошибки и валидацию входных данных.
@@ -218,12 +223,25 @@ Frontend не должен:
- OTP-only регистрацию и вход; - OTP-only регистрацию и вход;
- OTP по номеру телефона; проверка кода — в Keycloak (заглушка `KEYCLOAK_OTP_MOCK_*` или SMS-провайдер, см. arch-04 и «Поток авторизации»); - OTP по номеру телефона; проверка кода — в Keycloak (заглушка `KEYCLOAK_OTP_MOCK_*` или SMS-провайдер, см. arch-04 и «Поток авторизации»);
- продуктовые лимиты OTP (`otp.phone.*` из `app_settings`) через authenticator/SPI; счётчики попыток — в зоне Keycloak (Redis DB Keycloak/SPI или in-memory Keycloak), **не** в `api-backend`;
- хранение учетных записей; - хранение учетных записей;
- выдачу и обновление токенов; - выдачу и обновление токенов (access + refresh);
- настройку realm, clients, roles, policies. - настройку realm, clients, roles, policies;
- публикацию OIDC discovery и JWKS для проверки JWT.
Парольная авторизация, magic link и социальные логины не входят в MVP. Парольная авторизация, 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-провайдер | Keycloak → SMS (post-MVP) | Доставка OTP; на MVP — mock code из `.env` |
Confidential **backend client** Keycloak (client credentials) в MVP **не обязателен**: S2S между нашими сервисами идёт по service tokens, не через Keycloak. Client можно завести заранее в realm как optional для будущих admin/ops сценариев.
### Nginx Reverse Proxy ### Nginx Reverse Proxy
Отвечает за: Отвечает за:
@@ -231,13 +249,13 @@ Frontend не должен:
- прием внешнего HTTPS-трафика; - прием внешнего HTTPS-трафика;
- TLS termination; - TLS termination;
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»); - редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
- маршрутизацию `/api/*` и `/realtime/*` в api-backend; - маршрутизацию `/api/*` в api-backend (включая `WS /api/v1/realtime`);
- маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak; - маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak;
- маршрутизацию публичных `/bitrix/*` endpoint в `bitrix-local-app`; - маршрутизацию публичных `/bitrix/*` endpoint в `bitrix-local-app`;
- маршрутизацию `/bitrix/sync/*` webhook endpoint в `bitrix-sync`; - маршрутизацию `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
- защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`; - защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`;
- отсутствие публичной маршрутизации к `message-safety` — сервис доступен только из внутренней Docker-сети; - отсутствие публичной маршрутизации к `message-safety` — сервис доступен только из внутренней Docker-сети;
- передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`; - передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID` (если клиент не прислал `X-Request-ID` — nginx **генерирует** UUID и прокидывает upstream);
- базовые лимиты размера запроса и timeout; - базовые лимиты размера запроса и timeout;
- грубые edge rate limits по IP, route и зоне риска; - грубые edge rate limits по IP, route и зоне риска;
- TLS 1.2/1.3, HSTS, security headers и скрытие технологических заголовков; - TLS 1.2/1.3, HSTS, security headers и скрытие технологических заголовков;
@@ -266,48 +284,49 @@ Frontend не должен:
- сохранение сообщений, истории диалогов (CRM sync — зона `bitrix-sync`, не api-backend); - сохранение сообщений, истории диалогов (CRM sync — зона `bitrix-sync`, не api-backend);
- доставку в Bitrix24 Open Lines и realtime клиенту; - доставку в Bitrix24 Open Lines и realtime клиенту;
- проверку JWT, согласий, edge rate limits; - проверку JWT, согласий, edge rate limits;
- polling `task_id` на стороне клиента — только api-backend (фоновый worker или internal loop). - polling `task_id` на стороне клиента запрещён — только api-backend, и только **внутри** обработки `POST .../messages` (sync wait до финального вердикта).
api-backend не решает, sync или async нужна проверка: это определяет Message Safety Service по результатам фазы текста/ссылок и кэша файлов. api-backend не решает, sync или async нужна проверка внутри Message Safety: это определяет Message Safety Service. Но для клиента `POST .../messages` всегда завершается финальным allow/deny (или ошибкой timeout/зависимости).
## Гостевая сессия (до JWT) ## Гостевой режим (до JWT)
До OTP frontend работает в гостевом режиме с локально сгенерированным **`guest_session_id`** (UUID v4): До OTP frontend работает **локально** без записи согласий и UX-сессии в App DB:
- создаётся при первом запуске приложения, хранится в secure storage устройства; - UI главного экрана, популярные вопросы и публичный контент — через `GET /api/v1/public/*` (без JWT);
- передаётся в `POST /api/v1/consents` вместе с согласиями и device metadata; - pop-up согласий показывается **до** OTP, но факт принятия хранится **только на клиенте** до получения tokens;
- api-backend сохраняет согласия с привязкой к `guest_session_id` (TTL записи — 24 ч); - **`POST /consents`**, **`POST /analytics/session-start`** и остальные write/API чата — **только с JWT**;
- после успешного OTP api-backend **связывает** записи согласий и device session с `UserIdentity` по `keycloak_sub`; - опциональный локальный `guest_session_id` (UUID в secure storage) может использоваться frontend для своей аналитики/идемпотентности UI, но **не** является auth и **не** открывает backend write-endpoint.
- `guest_session_id` не используется для доступа к защищённым ресурсам после выдачи JWT.
## Аналитическая UX-сессия (`ux_session_id`) ## Аналитическая UX-сессия (`ux_session_id`)
**UX-сессия** — период непрерывной активности пользователя в приложении для аналитики и сквозной трассировки. Это **не** сессия Keycloak, **не** refresh/access token и **не** механизм авторизации. **UX-сессия** — период непрерывной активности **авторизованного** пользователя в приложении для аналитики и сквозной трассировки. Это **не** сессия Keycloak, **не** refresh/access token и **не** механизм авторизации.
### Роли компонентов ### Роли компонентов
**Frontend** (источник истины по правилам сессии): **Frontend** (источник истины по правилам сессии):
- хранит `ux_session_id` и `last_activity_at` **только в памяти** (не в localStorage/secure storage); - хранит `ux_session_id` и `last_activity_at` **только в памяти** (не в localStorage/secure storage);
- при новой сессии вызывает `POST /api/v1/analytics/session-start` и сохраняет полученный `ux_session_id`; - вызывает `POST /api/v1/analytics/session-start` **только при наличии JWT** (после OTP или silent refresh);
- в гостевом режиме `session-start` **не** вызывается;
- при новой сессии сохраняет полученный `ux_session_id`;
- обновляет `last_activity_at` при пользовательской активности и при возврате из фона; - обновляет `last_activity_at` при пользовательской активности и при возврате из фона;
- при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия; - при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия (снова с JWT);
- передаёт **`X-Ux-Session-Id`** во **всех** запросах к backend (public и JWT). - передаёт **`X-Ux-Session-Id`** во **всех** JWT-запросах к backend, пока сессия активна.
**api-backend**: **api-backend**:
1. принимает `session_start`, создаёт запись **`UxSession`**, возвращает `ux_session_id`; 1. принимает `session_start` **только с валидным JWT**, создаёт запись **`UxSession`** с `user_id`, возвращает `ux_session_id`;
2. пишет analytics/audit-событие `session_start` (без PII); 2. пишет analytics/audit-событие `session_start` (без PII);
3. включает `ux_session_id` из заголовка в JSON-логи (если передан); 3. включает `ux_session_id` из заголовка в JSON-логи (если передан);
4. **не** блокирует запросы при отсутствии или неизвестном `ux_session_id` — это не auth. 4. отсутствие `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту) — это не auth, но сам `session-start` без JWT недоступен.
`request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов. `request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов.
## Поток возврата пользователя (без OTP) ## Поток возврата пользователя (без OTP)
1. Клиент открывает приложение (UX-сессия определяется по правилам выше, независимо от auth). 1. Клиент открывает приложение. Пока нет JWT — гостевой UI; `session-start` не вызывается.
2. Frontend проверяет наличие refresh token в secure storage. 2. Frontend проверяет наличие refresh token в secure storage.
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**. 3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**, затем при необходимости `POST /analytics/session-start`.
4. Frontend работает как авторизованный пользователь (история, профиль, чат). 4. Frontend работает как авторизованный пользователь (история, профиль, чат).
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP. 5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
@@ -343,35 +362,39 @@ api-backend не решает, sync или async нужна проверка: э
## Создание диалога (MVP) ## Создание диалога (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` и использует его далее. - Frontend перед `POST .../messages` вызывает `POST /api/v1/dialogs` (заголовок `Idempotency-Key`), получает `dialog_id` и использует его далее.
- Популярный вопрос: после auth тот же порядок — `POST /dialogs``POST .../messages` с текстом вопроса. - Популярный вопрос: после auth тот же порядок — `POST /dialogs``POST .../messages` с текстом вопроса.
- `dialog_id` = `external_chat_id` для Open Lines (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Идентификаторы»). - `dialog_id` = `external_chat_id` для Open Lines (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Идентификаторы»).
- При первой доставке в Bitrix24 `bitrix-local-app` создаёт запись `dialog_sessions`. - При первой доставке в Bitrix24 `bitrix-local-app` создаёт запись `dialog_sessions`.
- Новый активный диалог после `closed` — снова через `POST /dialogs` (когда продукт это разрешит; MVP: один активный в любой момент).
## Поток авторизации (OTP) ## Поток авторизации (OTP)
Срабатывает, когда клиент **ещё не имеет действующего refresh token** (первый вход) или refresh token **истёк**. Если refresh token валиден — см. «Поток возврата пользователя». Срабатывает, когда клиент **ещё не имеет действующего refresh token** (первый вход) или refresh token **истёк**. Если refresh token валиден — см. «Поток возврата пользователя».
1. Клиент находится в гостевом режиме (`guest_session_id` уже создан). 1. Клиент в гостевом режиме (только UI + `GET /api/v1/public/*`).
2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос). 2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
3. Frontend показывает pop-up с тремя согласиями. 3. Frontend показывает pop-up с тремя согласиями; факт принятия хранится **локально** до OTP.
4. Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение. 4. Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение.
5. Клиент может опционально согласиться на рекламные коммуникации. 5. Клиент может опционально согласиться на рекламные коммуникации.
6. Если обязательные согласия не даны, отправка блокируется. 6. Если обязательные согласия не даны, отправка блокируется.
7. Frontend вызывает `POST /api/v1/consents` с `guest_session_id`, версиями документов, device metadata, IP/user agent (через backend). 7. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
8. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP). 8. Keycloak запускает OTP-flow по телефону: клиент вводит номер, инициируется «отправка» OTP (при заглушке SMS фактически не уходит — см. arch-04).
9. Keycloak запускает OTP-flow по телефону: клиент вводит номер, инициируется «отправка» OTP (при заглушке SMS фактически не уходит — см. arch-04). 9. Лимиты OTP на **edge**`nginx` (`NGINX_RATE_LIMIT_AUTH`); продуктовые лимиты `otp.phone.*` из `app_settings` применяются на стороне **Keycloak authenticator / SPI** (или обёртки OTP), не в `api-backend`. До интеграции SMS (mock OTP) достаточно edge + mock code.
10. Лимиты OTP проверяются по **`app_settings`** (`otp.phone.*`). 10. Клиент вводит OTP и отправляет его в Keycloak.
11. Клиент вводит OTP и отправляет его в Keycloak. 11. **Keycloak проверяет корректность введённого OTP**:
12. **Keycloak проверяет корректность введённого OTP**:
- при **`KEYCLOAK_OTP_MOCK_ENABLED=true`** (MVP и любой режим с включённой заглушкой): введённое значение должно **совпадать** с `KEYCLOAK_OTP_MOCK_CODE` из `.env`; - при **`KEYCLOAK_OTP_MOCK_ENABLED=true`** (MVP и любой режим с включённой заглушкой): введённое значение должно **совпадать** с `KEYCLOAK_OTP_MOCK_CODE` из `.env`;
- при **`KEYCLOAK_OTP_MOCK_ENABLED=false`** (после интеграции с SMS-провайдером, см. бэклог): введённое значение должно **совпадать** с одноразовым OTP, сгенерированным Keycloak и отправленным провайдером на телефон клиента (с учётом TTL и лимита попыток). - при **`KEYCLOAK_OTP_MOCK_ENABLED=false`** (после интеграции с SMS-провайдером, см. бэклог): введённое значение должно **совпадать** с одноразовым OTP, сгенерированным Keycloak и отправленным провайдером на телефон клиента (с учётом TTL и лимита попыток).
- при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 13 не выполняется. - при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 12 не выполняется.
13. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE. 12. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
14. Frontend вызывает **`POST /api/v1/auth/bootstrap`** с JWT и `guest_session_id` (см. arch-02). 13. Frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** — в теле передаёт локально принятые согласия и device metadata (см. arch-02). api-backend атомарно: `find-or-create` по JWT `sub` (`keycloak_sub`), телефон из JWT claims (не из body) → сохранение `UserConsent` на `user_id` → минимальный профиль.
15. api-backend выполняет `find-or-create` пользователя, связывает согласия с `guest_session_id`, при необходимости привязывает `user_id` к текущей **`UxSession`** по `ux_session_id`. 14. Frontend вызывает **`POST /api/v1/analytics/session-start`** (если нужна новая UX-сессия) и далее работает с `X-Ux-Session-Id`.
16. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM. 15. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM.
17. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата). 16. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
Отдельный **`POST /api/v1/consents`** после первого входа нужен, когда пользователь заново принимает обновлённые версии документов (не часть OTP-flow).
## Поток работы с чатом: клиент -> Битрикс24 ## Поток работы с чатом: клиент -> Битрикс24
@@ -386,20 +409,21 @@ api-backend не решает, sync или async нужна проверка: э
**Файловое сообщение:** **Файловое сообщение:**
1. Frontend инициализирует **одно** вложение (`POST .../attachments/init`), загружает файл; api-backend сохраняет его в **S3-quarantine**. 1. Frontend инициализирует **одно** вложение (`POST .../attachments/init`), получает **presigned PUT** в **S3-quarantine**, загружает байты **напрямую в S3**, затем вызывает `POST .../attachments/{attachment_id}/complete`.
2. Frontend отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с `attachment_id` и `checksum` (поле `text` пустое). 2. Frontend отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с `attachment_id` и `checksum` (поле `text` пустое).
3. Nginx и API применяют rate limits. 3. Nginx и API применяют rate limits.
4. API **синхронно** вызывает Message Safety Service — шаг проверки файла (текст и ссылки пропускаются, если `text` пуст). 4. API **синхронно** вызывает Message Safety Service — шаг проверки файла (текст и ссылки пропускаются, если `text` пуст).
**Общая ветка вердикта (оба типа):** **Общая ветка вердикта (оба типа):**
5. **`403 deny`**: API удаляет quarantine (если был файл), возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит. 5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
6. **`200 allow`**: API переносит файл в S3-data (если был), сохраняет сообщение, отправляет в Bitrix24, подтверждает клиенту (realtime/polling). 6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=delivered`), отправляет в Bitrix24, подтверждает клиенту; `Dialog.status``waiting_for_company`.
7. **`203 pending` + `task_id`**: api-backend сохраняет сообщение со статусом ожидания проверки, отвечает клиенту, что сообщение обрабатывается; quarantine не трогает. 7. **`203 pending` + `task_id`**: api-backend пишет checkpoint в `safety_tasks` и **регулярно синхронно** вызывает `GET /internal/safety/v1/messages/tasks/{task_id}` (backoff), пока не получит финальный вердикт или не истечёт `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Пока идёт poll, **этот** клиентский `POST .../messages` ещё не завершён (соединение ждёт). Параллельные запросы других клиентов **не** блокируются — общей очереди анализа на api-backend нет.
8. Фоновый процесс API опрашивает `GET /internal/safety/v1/messages/tasks/{task_id}`: - финальный **`200 allow`** → как п. 6, затем ответ клиенту;
- финальный **`200 allow`** → S3-data, Bitrix24, статус «доставлено», realtime клиенту; - финальный **`403 deny`** → как п. 5, затем ответ клиенту;
- финальный **`403 deny`** → удаление quarantine, статус «отклонено», уведомление клиенту; - timeout / недоступность safety → `delivery_status=failed`, безопасная ошибка клиенту (`503` / `504`), quarantine не promote; recovery по `safety_tasks` — зона модуля.
- **`203 pending`** → повтор опроса с backoff.
Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
## Поток работы с чатом: Битрикс24 -> клиент ## Поток работы с чатом: Битрикс24 -> клиент
@@ -408,9 +432,9 @@ api-backend не решает, sync или async нужна проверка: э
3. `bitrix-local-app` проверяет `application_token`, нормализует payload и сохраняет idempotent inbox. 3. `bitrix-local-app` проверяет `application_token`, нормализует payload и сохраняет idempotent inbox.
4. `bitrix-local-app` обогащает событие данными из `dialog_sessions` и forward-ит в API, если `BITRIX_API_FORWARD_URL` включен. 4. `bitrix-local-app` обогащает событие данными из `dialog_sessions` и forward-ит в API, если `BITRIX_API_FORWARD_URL` включен.
5. api-backend находит локальный диалог по `external_chat_id` (= `dialog_id`, см. [`arch-00-glossary.md`](arch-00-glossary.md)). 5. api-backend находит локальный диалог по `external_chat_id` (= `dialog_id`, см. [`arch-00-glossary.md`](arch-00-glossary.md)).
6. api-backend сохраняет входящее сообщение в App DB (`sender_type=company`), а файл — в Selectel S3 (documents) с metadata в App DB. По факту сообщения API обновляет `Dialog.status`: входящее от оператора → `waiting_for_client`, исходящее от клиента → `waiting_for_company`. 6. 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).
7. `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery`. 7. `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery`.
8. api-backend публикует событие для frontend через WebSocket/SSE. Если realtime недоступен, frontend получает сообщение через polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`. 8. api-backend публикует событие для frontend через **WebSocket** (`WS /api/v1/realtime`). Если realtime недоступен, frontend получает сообщение через polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
9. Frontend отображает сообщение оператора в чате. 9. Frontend отображает сообщение оператора в чате.
10. При получении от `bitrix-local-app` доменного события `dialog.closed` (Bitrix24 `ONIMCONNECTORDIALOGFINISH`) API переводит `Dialog.status` в `closed`. 10. При получении от `bitrix-local-app` доменного события `dialog.closed` (Bitrix24 `ONIMCONNECTORDIALOGFINISH`) API переводит `Dialog.status` в `closed`.
@@ -453,7 +477,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
## Аудит скачиваний ## Аудит скачиваний
При выдаче presigned URL на скачивание (`GET .../download-url`, вложения чата) api-backend пишет audit-событие в App DB: При выдаче 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:
| Поле | Значение | | Поле | Значение |
|---|---| |---|---|
@@ -471,14 +495,15 @@ App DB — **локальный кэш** для UI. Двусторонний syn
Детальный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «Realtime». Детальный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «Realtime».
- Transport: WebSocket `WS /api/v1/realtime` (JWT). - Transport: **только WebSocket** `WS /api/v1/realtime` (JWT). SSE в MVP **не** используется.
- Путь входит в `/api/*`; отдельный location `/realtime/*` в nginx **не** нужен.
- Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`. - Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
- События: новое сообщение, смена статуса сообщения/диалога. - События: новое сообщение, смена `delivery_status` / `safety_status`, смена `Dialog.status`.
## Принципы безопасности ## Принципы безопасности
- Все защищенные пользовательские API требуют валидный JWT. - Все защищенные пользовательские API требуют валидный JWT.
- Гостевые API доступны только для публичных настроек и стартового контента. - Без JWT доступны **только** read-only публичные endpoint: `GET /api/v1/public/*` (rate limit + CORS + кэш). Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) требуют JWT.
- Все внешние пользовательские соединения работают через HTTPS. - Все внешние пользовательские соединения работают через HTTPS.
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается. - HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
- TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети. - TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети.
@@ -487,17 +512,18 @@ App DB — **локальный кэш** для UI. Двусторонний syn
- INPUT-validation на api-backend - INPUT-validation на api-backend
- использовать только Параметризованные SQL-запросы - использовать только Параметризованные SQL-запросы
- обязательное Экранирование вывода - обязательное Экранирование вывода
- настройка CORS только на разрешенные домены (указать в .env) - настройка CORS только на разрешённые домены (`security.cors.allowed_origins` в `app_settings`, см. arch-04)
- настройка Secure Headers (CSP, X-Frame-Options и др.) - настройка Secure Headers (CSP, X-Frame-Options и др.)
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `user_id`. - Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `user_id`.
- Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос. - Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос.
- Все публичные id создаются в формате UUID. - Все публичные id создаются в формате UUID.
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (перечень переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Service tokens (internal API)»). - Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (перечень переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Service tokens (internal API)»).
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis. - Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check``200` | `403` | `203`; при `203` API опрашивает `task_id` до финального вердикта. - Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check`при `203` api-backend синхронно поллит `task_id` до финального `200`/`403` (или timeout); без очереди анализа на api-backend.
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`. - Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
- Клиент **не пишет** напрямую в S3; загрузка только через `api-backend`. - У клиента **нет** постоянных S3 credentials. Загрузка — **presigned PUT** в S3-quarantine, выданный `api-backend`; скачивание — **presigned GET**. Байты файла **не** проксируются через `api-backend`.
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты. - `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`](arch-04-settings-and-content.md)). - Все изменяемые параметры, телефоны, лимиты, mime types и флаги хранятся в настройках ([`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
- PII-данные не пишутся в логи в открытом виде. - PII-данные не пишутся в логи в открытом виде.
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний. - Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.
+160 -54
View File
@@ -13,7 +13,7 @@
- Internal HTTP API между backend-сервисами используют единую маску: **`/internal/{service_mnemonic}/v1/{resource}`**, где `{service_mnemonic}` — короткое имя владельца endpoint (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Мнемоники internal API»). Health-check остаётся на `/health/*`. - Internal HTTP API между backend-сервисами используют единую маску: **`/internal/{service_mnemonic}/v1/{resource}`**, где `{service_mnemonic}` — короткое имя владельца endpoint (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Мнемоники internal API»). Health-check остаётся на `/health/*`.
- OpenAPI 3.1 обязателен для HTTP-контрактов `api-backend`, `message-safety`, `bitrix-sync` и `bitrix-local-app` — файлы `{service}/openapi.yaml` в репозитории сервиса (см. раздел «OpenAPI»); для Bitrix24 REST фиксируются используемые методы и payload-мэппинг. - OpenAPI 3.1 обязателен для HTTP-контрактов `api-backend`, `message-safety`, `bitrix-sync` и `bitrix-local-app` — файлы `{service}/openapi.yaml` в репозитории сервиса (см. раздел «OpenAPI»); для Bitrix24 REST фиксируются используемые методы и payload-мэппинг.
- Все service-to-service вызовы передают `X-Request-ID` и по возможности W3C `traceparent`. - Все service-to-service вызовы передают `X-Request-ID` и по возможности W3C `traceparent`.
- Frontend передаёт **`X-Ux-Session-Id`** во всех запросах к `api-backend`, когда UX-сессия активна (рекомендуется для аналитики и логов; **не** является auth). - Frontend передаёт **`X-Ux-Session-Id`** во всех JWT-запросах к `api-backend`, когда UX-сессия активна (рекомендуется для аналитики и логов; **не** является auth). `session-start` и `consents` требуют JWT.
- Все internal API защищаются service token и закрытой Docker/VPC-сетью. - Все internal API защищаются service token и закрытой Docker/VPC-сетью.
## Service tokens (internal API) ## Service tokens (internal API)
@@ -49,20 +49,21 @@
|---|---|---|---|---| |---|---|---|---|---|
| `GET /api/v1/public/app-config` | `api-backend` | Expo frontend | Публичные настройки: OTP, оператор, лимиты, файлы, **UX idle timeout** | public + CORS/rate limit | | `GET /api/v1/public/app-config` | `api-backend` | Expo frontend | Публичные настройки: OTP, оператор, лимиты, файлы, **UX idle timeout** | public + CORS/rate limit |
| `GET /api/v1/public/content` | `api-backend` | Expo frontend | Тексты по мнемоникам и популярные вопросы | public + CORS/rate limit | | `GET /api/v1/public/content` | `api-backend` | Expo frontend | Тексты по мнемоникам и популярные вопросы | public + CORS/rate limit |
| `POST /api/v1/consents` | `api-backend` | Expo frontend | Сохранение согласий перед OTP; тело включает `guest_session_id`, версии документов, device metadata | public + `guest_session_id` + rate limit | | `POST /api/v1/consents` | `api-backend` | Expo frontend | Повторное сохранение согласий (новые версии документов); привязка к `user_id` | JWT + rate limit |
| `POST /api/v1/analytics/session-start` | `api-backend` | Expo frontend | Событие `session_start`, новая `UxSession` | public + rate limit | | `POST /api/v1/analytics/session-start` | `api-backend` | Expo frontend | Событие `session_start`, новая `UxSession` (только для авторизованного пользователя) | JWT + rate limit |
| `POST /api/v1/auth/bootstrap` | `api-backend` | Expo frontend | После OTP: `find-or-create` пользователя, связь согласий, привязка `user_id` к `UxSession` | JWT | | `POST /api/v1/auth/bootstrap` | `api-backend` | Expo frontend | После OTP: `find-or-create` пользователя + сохранение согласий из тела запроса | JWT |
| `POST /api/v1/dialogs` | `api-backend` | Expo frontend | Создание диалога перед первым сообщением (в т.ч. после популярного вопроса) | JWT + idempotency | | `POST /api/v1/dialogs` | `api-backend` | Expo frontend | Создание диалога перед первым сообщением (в т.ч. после популярного вопроса) | JWT + idempotency |
| `GET /api/v1/me` | `api-backend` | Expo frontend | Профиль текущего клиента | JWT | | `GET /api/v1/me` | `api-backend` | Expo frontend | Профиль текущего клиента | JWT |
| `GET /api/v1/me/documents` | `api-backend` | Expo frontend | Список документов (MVP: может быть пустым; доставка — post-MVP) | JWT | | `GET /api/v1/me/documents` | `api-backend` | Expo frontend | Список документов (MVP: может быть пустым; доставка — post-MVP) | JWT |
| `GET /api/v1/documents/{document_id}` | `api-backend` | Expo frontend | Метаданные документа (post-MVP) | JWT | | `GET /api/v1/documents/{document_id}` | `api-backend` | Expo frontend | Метаданные документа (post-MVP) | JWT |
| `GET /api/v1/documents/{document_id}/download-url` | `api-backend` | Expo frontend | Presigned URL; обязателен audit | JWT | | `GET /api/v1/documents/{document_id}/download-url` | `api-backend` | Expo frontend | Presigned URL документа профиля; обязателен audit | JWT |
| `GET /api/v1/dialogs` | `api-backend` | Expo frontend | История диалогов | JWT | | `GET /api/v1/dialogs` | `api-backend` | Expo frontend | История диалогов | JWT |
| `GET /api/v1/dialogs/{dialog_id}` | `api-backend` | Expo frontend | Карточка диалога | JWT | | `GET /api/v1/dialogs/{dialog_id}` | `api-backend` | Expo frontend | Карточка диалога | JWT |
| `GET /api/v1/dialogs/{dialog_id}/messages` | `api-backend` | Expo frontend | История сообщений, polling fallback | JWT | | `GET /api/v1/dialogs/{dialog_id}/messages` | `api-backend` | Expo frontend | История сообщений, polling fallback | JWT |
| `POST /api/v1/dialogs/{dialog_id}/messages` | `api-backend` | Expo frontend | Отправка сообщения клиента (MVP: `content_kind` `text` или `file`, см. ниже) | JWT + idempotency + safety | | `POST /api/v1/dialogs/{dialog_id}/messages` | `api-backend` | Expo frontend | Отправка сообщения клиента (MVP: `content_kind` `text` или `file`, см. ниже) | JWT + idempotency + safety |
| `POST /api/v1/dialogs/{dialog_id}/attachments/init` | `api-backend` | Expo frontend | Инициализация загрузки в S3-quarantine | JWT | | `POST /api/v1/dialogs/{dialog_id}/attachments/init` | `api-backend` | Expo frontend | Инициализация загрузки; ответ: `attachment_id` + **presigned PUT** в S3-quarantine | JWT |
| `POST /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/complete` | `api-backend` | Expo frontend | Завершение загрузки и фиксация checksum/metadata | JWT | | `POST /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/complete` | `api-backend` | Expo frontend | Подтверждение загрузки, проверка объекта в quarantine, фиксация checksum/metadata | JWT |
| `GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url` | `api-backend` | Expo frontend | Presigned URL вложения чата; обязателен audit | JWT |
| `WS /api/v1/realtime` | `api-backend` | Expo frontend | Realtime-события чата, статусы доставки, unread | JWT | | `WS /api/v1/realtime` | `api-backend` | Expo frontend | Realtime-события чата, статусы доставки, unread | JWT |
Единый формат ошибки: Единый формат ошибки:
@@ -78,11 +79,27 @@
} }
``` ```
### `POST /api/v1/consents` (тело запроса) ### `POST /api/v1/auth/bootstrap` (после OTP)
Вызывается **один раз** после успешного OTP и получения JWT. Создаёт локального пользователя и **сразу** сохраняет согласия из тела (атомарно в одной транзакции). **Не** создаёт UX-сессию — для этого `POST /analytics/session-start`.
**Заголовки:** `Authorization: Bearer <access_token>`**единственный** источник идентичности пользователя.
**Как привязываются согласия (не через тело JSON):**
| Источник | Поле | Назначение |
|---|---|---|
| JWT claim `sub` | → `UserIdentity.keycloak_sub` | find-or-create пользователя; FK для `UserConsent.user_id` |
| JWT claim телефона | → `UserIdentity.phone_number` | номер, подтверждённый OTP в Keycloak (master auth-телефона) |
| Тело `consents` | версии / accepted | что именно принял пользователь |
| Тело `device` | metadata | устройство; не идентичность |
Телефон **не** передаётся в JSON body: клиент мог бы подставить чужой номер. api-backend читает телефон из **claims access token** Keycloak (канонический claim — по настройке realm; типично `phone_number` или `preferred_username` в E.164). Если claim отсутствует — **`400`** `phone_claim_missing`.
**Тело:**
```json ```json
{ {
"guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
"consents": { "consents": {
"personal_data": { "accepted": true, "version": "2026-06-10" }, "personal_data": { "accepted": true, "version": "2026-06-10" },
"user_agreement": { "accepted": true, "version": "2026-06-10" }, "user_agreement": { "accepted": true, "version": "2026-06-10" },
@@ -96,20 +113,41 @@
} }
``` ```
После OTP api-backend связывает запись с `UserIdentity` по `guest_session_id` (в рамках `POST /api/v1/auth/bootstrap`). TTL guest-записи — 24 ч. **Порядок после OTP:** `POST /auth/bootstrap``POST /analytics/session-start` → чат.
**Ответ `200`:** `{ "user_id": "uuid", "profile_ready": true }`.
**Ошибки:** `401` (JWT), `403` `consents_required` (обязательные согласия не `accepted: true`), `400` (невалидные версии/тело / нет phone claim).
### `POST /api/v1/consents` (повторное принятие)
Не часть OTP-flow. Используется, когда уже есть `UserIdentity` и нужно зафиксировать **новые версии** документов (или повторное принятие).
**Заголовки:** `Authorization: Bearer <access_token>`, `X-Ux-Session-Id` (если есть).
```json
{
"consents": {
"personal_data": { "accepted": true, "version": "2026-06-10" },
"user_agreement": { "accepted": true, "version": "2026-06-10" },
"marketing": { "accepted": false, "version": "2026-06-10" }
}
}
```
api-backend сохраняет согласия с привязкой к **`user_id`** из JWT. Пользователь должен уже существовать (`bootstrap` выполнен), иначе **`404`** / **`409`** по контракту модуля. Обязательные согласия без `accepted: true`**`403`** `consents_required`.
### `POST /api/v1/analytics/session-start` (событие `session_start`) ### `POST /api/v1/analytics/session-start` (событие `session_start`)
Вызывается frontend **только** при начале **новой** UX-сессии (см. arch-01, «Аналитическая UX-сессия»). **Не** привязан к OTP и JWT. Вызывается frontend **только** при начале **новой** UX-сессии у **авторизованного** пользователя (есть валидный JWT и обычно уже выполнен `bootstrap`). **Не** вызывается в гостевом режиме.
**Заголовки:** `X-Request-ID` (опционально). **Заголовки:** `Authorization: Bearer <access_token>`, `X-Request-ID` (опционально).
**Тело:** **Тело:**
```json ```json
{ {
"start_reason": "first_launch", "start_reason": "first_launch",
"guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
"device": { "device": {
"platform": "web", "platform": "web",
"app_version": "1.0.0", "app_version": "1.0.0",
@@ -118,8 +156,7 @@
} }
``` ```
- `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`; - `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`.
- `guest_session_id` — опционально (если уже создан в гостевом режиме).
**Ответ `201`:** **Ответ `201`:**
@@ -134,31 +171,26 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
**Повторный вызов в рамках той же UX-сессии не требуется** (возврат из фона в пределах idle timeout). **Повторный вызов в рамках той же UX-сессии не требуется** (возврат из фона в пределах idle timeout).
### `POST /api/v1/auth/bootstrap` (после OTP) **Ошибки:** `401` без/с невалидным JWT.
Вызывается **один раз** после успешного OTP и получения JWT. **Не** создаёт UX-сессию.
**Заголовки:** `Authorization: Bearer <access_token>`, `X-Ux-Session-Id` (рекомендуется).
**Тело:**
```json
{
"guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
"ux_session_id": "660e8400-e29b-41d4-a716-446655440001"
}
```
**Ответ `200`:** `{ "user_id": "uuid", "profile_ready": true }`.
**Ошибки:** `401` (JWT), `403` (согласия не приняты / guest session истёк).
### Создание диалога ### Создание диалога
- `POST /api/v1/dialogs`idempotency key в заголовке; ответ `{ "dialog_id": "uuid", "status": "open" }`. - `POST /api/v1/dialogs`заголовок **`Idempotency-Key`** (обязателен); ответ `{ "dialog_id": "uuid", "status": "open" | "waiting_for_company" | "waiting_for_client" }`.
- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth). - **Один активный диалог** на пользователя: если уже есть диалог со статусом не `closed`, endpoint возвращает его (idempotent), новый не создаёт.
- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth), если у клиента ещё нет `dialog_id`.
- `dialog_id` = `external_chat_id` (см. [`arch-00-glossary.md`](arch-00-glossary.md)). - `dialog_id` = `external_chat_id` (см. [`arch-00-glossary.md`](arch-00-glossary.md)).
### Idempotency (G1)
| Правило | Значение |
|---|---|
| Заголовок | `Idempotency-Key` (UUID или opaque string ≤ 128 символов) |
| Endpoint | `POST /api/v1/dialogs`, `POST /api/v1/dialogs/{dialog_id}/messages` (и другие mutating POST по OpenAPI) |
| Хранение | Redis (DB `/0` api-backend): ключ → ответ / request fingerprint |
| TTL | **24 часа** |
| Повтор с тем же ключом и тем же телом | тот же HTTP-ответ, без повторного side-effect |
| Повтор с тем же ключом и **другим** телом | **`409`** `idempotency_key_reused` |
### Формат исходящего сообщения клиента (MVP) ### Формат исходящего сообщения клиента (MVP)
Имена `content_kind`, полей — [`arch-00-glossary.md`](arch-00-glossary.md). Правила: Имена `content_kind`, полей — [`arch-00-glossary.md`](arch-00-glossary.md). Правила:
@@ -169,12 +201,28 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
| `file` | `attachment_id` + `checksum`; `text` пустой | пустая строка | ровно 1 запись | | `file` | `attachment_id` + `checksum`; `text` пустой | пустая строка | ровно 1 запись |
- непустой `text` **и** вложение → **`400`** `mixed_content_not_allowed` (до `message-safety`); - непустой `text` **и** вложение → **`400`** `mixed_content_not_allowed` (до `message-safety`);
- пустое сообщение → **`403`** `empty_message`; - пустое сообщение (нет `text` и нет `attachment_id`)**`400`** `empty_message`;
- более одного вложения → **`400`** `too_many_attachments`; - более одного вложения → **`400`** `too_many_attachments`;
- файловое сообщение в Bitrix24: `message.files` (signed URL), `message.text` пустой. - файловое сообщение в Bitrix24: `message.files` (signed URL), `message.text` пустой.
Post-MVP: допускается «текст + файлы» отдельной версией API. Post-MVP: допускается «текст + файлы» отдельной версией API.
### Загрузка вложения (MVP)
Байты файла идут **напрямую в S3-quarantine** по короткоживущему **presigned URL**. `api-backend` не проксирует тело файла: выдаёт URL, проверяет результат, управляет lifecycle (promote/delete).
1. `POST .../attachments/init` (JWT) → `{ attachment_id, upload_url, upload_headers?, expires_at }``upload_url` = **presigned PUT** (или POST policy) в **S3-quarantine**; ключ объекта и ограничения (bucket, key prefix, `Content-Type`, max size) задаёт `api-backend`.
2. Frontend загружает байты **напрямую в Selectel S3** по `upload_url` (не через `api-backend`).
3. `POST .../attachments/{attachment_id}/complete` с `checksum` (SHA-256) → api-backend проверяет наличие объекта в quarantine (HeadObject / размер / checksum), фиксирует metadata, `scan_status=pending`.
4. `POST .../messages` с `attachment_id` + `checksum` → Message Safety.
Правила безопасности:
- у клиента **нет** постоянных S3 access keys — только одноразовый/короткий presigned URL;
- presigned URL разрешает запись **только** в выделенный key в S3-quarantine (не в S3-data);
- TTL URL короткий (константа модуля / `app_settings`, ориентир минуты);
- скачивание из S3-data — отдельные **presigned GET** через `.../download-url` (с audit).
## Realtime (`WS /api/v1/realtime`) ## Realtime (`WS /api/v1/realtime`)
Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` или subprotocol (реализация — в модуле `api-backend`). Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` или subprotocol (реализация — в модуле `api-backend`).
@@ -196,8 +244,8 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
| `type` | Назначение | Ключевые поля | | `type` | Назначение | Ключевые поля |
|---|---|---| |---|---|---|
| `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) | | `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) |
| `message.status` | Смена статуса доставки/safety | `dialog_id`, `message_id`, `status` | | `message.status` | Смена `safety_status` / `delivery_status` | `dialog_id`, `message_id`, `safety_status`, `delivery_status` |
| `dialog.status` | Смена статуса диалога | `dialog_id`, `status` | | `dialog.status` | Смена `Dialog.status` | `dialog_id`, `status` |
**Reconnect:** **Reconnect:**
@@ -224,16 +272,30 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
## Frontend ↔ Keycloak ## Frontend ↔ Keycloak
Keycloak **обязателен** в production-like контуре с первого запуска (OTP, tokens, JWKS).
| Контракт | Владелец | Потребитель | Назначение | | Контракт | Владелец | Потребитель | Назначение |
|---|---|---|---| |---|---|---|---|
| OIDC Authorization Code Flow with PKCE | Keycloak | Expo frontend | OTP-only login, token issue, refresh | | OIDC Authorization Code Flow with PKCE | Keycloak | Expo frontend | OTP-only login, token issue |
| OIDC Refresh Token Grant | Keycloak | Expo frontend | Обновление access token без OTP при действующем refresh token | | OIDC Refresh Token Grant | Keycloak | Expo frontend | Обновление access token без OTP при действующем refresh token |
| OIDC logout | Keycloak | Expo frontend | Завершение сессии Keycloak, очистка tokens | | OIDC logout | Keycloak | Expo frontend | Завершение сессии Keycloak, очистка tokens |
| JWKS / discovery | Keycloak | Expo frontend, `api-backend` | Проверка issuer, audience и ключей | | OIDC Discovery (`/.well-known/openid-configuration`) | Keycloak | Expo frontend, `api-backend` | issuer, token/jwks endpoints |
| JWKS | Keycloak | `api-backend` | Проверка подписи access token (issuer, audience, exp) |
| OTP authenticator / SPI | Keycloak | — | Проверка OTP; product limits `otp.phone.*`; mock или SMS |
| PostgreSQL schema `keycloak` | Keycloak | Managed PostgreSQL | Учётные записи IdP |
Frontend не обращается напрямую к Keycloak DB и не хранит парольные credentials. Парольная авторизация в MVP отключена. Frontend не обращается напрямую к Keycloak DB и не хранит парольные credentials. Парольная авторизация в MVP отключена.
**OTP (Keycloak):** единственный канал первичной авторизации — телефон. OTP-flow нужен, когда refresh token отсутствует или истёк. При действующем refresh token frontend использует **Refresh Token Grant** и не показывает OTP. После ввода кода **Keycloak проверяет OTP**: при `KEYCLOAK_OTP_MOCK_ENABLED=true` — сверка с `KEYCLOAK_OTP_MOCK_CODE` (`.env`); при `false` — сверка с OTP от SMS-провайдера (post-MVP, [`!Backlog.md`](../../HAN_chat/!Backlog.md)). `api-backend` OTP не проверяет, только JWT. **`api-backend` ↔ Keycloak:** только **валидация JWT** по JWKS/discovery (кэш ключей). Admin REST / User API Keycloak в hot path **не** используются. Телефон и `sub` для `bootstrap` берутся из claims access token.
**OTP (Keycloak):** единственный канал первичной авторизации — телефон. OTP-flow нужен, когда refresh token отсутствует или истёк. При действующем refresh token frontend использует **Refresh Token Grant** и не показывает OTP. После ввода кода **Keycloak проверяет OTP**: при `KEYCLOAK_OTP_MOCK_ENABLED=true` — сверка с `KEYCLOAK_OTP_MOCK_CODE` (`.env`); при `false` — сверка с OTP от SMS-провайдера (post-MVP, [`!Backlog.md`](../../HAN_chat/!Backlog.md)). Счётчики и product limits OTP — **только** Keycloak/SPI (+ nginx edge); `api-backend` OTP **не** проверяет и **не** ведёт OTP counters в Redis.
**Clients в realm (MVP):**
| Client | Тип | Назначение |
|---|---|---|
| Frontend (Expo) | public + PKCE | login / refresh / logout |
| Backend confidential | optional | не нужен для hot path; зарезервирован под будущие admin/ops S2S |
### Жизненный цикл access token (frontend) ### Жизненный цикл access token (frontend)
@@ -258,19 +320,31 @@ Frontend не обращается напрямую к Keycloak DB и не хр
| Контракт | Владелец | Потребитель | Назначение | Защита | | Контракт | Владелец | Потребитель | Назначение | Защита |
|---|---|---|---|---| |---|---|---|---|---|
| `POST /internal/safety/v1/messages/check` | `message-safety` | `api-backend` | Синхронная проверка текста, ссылок и файлов по cache/rules | internal network + `X-Service-Token` | | `POST /internal/safety/v1/messages/check` | `message-safety` | `api-backend` | Проверка текста, ссылок и файлов | internal network + `X-Service-Token` |
| `GET /internal/safety/v1/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос async-проверки файлов | internal network + `X-Service-Token` | | `GET /internal/safety/v1/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос до финального вердикта **внутри** того же `POST .../messages` | internal network + `X-Service-Token` |
| Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key | | Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key |
HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend` не выбирает sync/async режим, а только интерпретирует ответ. HTTP-семантика от `message-safety`: `200 allow`, `403 deny`, `203 pending`.
Маппинг в App DB (`Message.safety_status` — см. [`arch-00-glossary.md`](arch-00-glossary.md)): Поведение `api-backend`:
| HTTP / `message-safety` | `Message.safety_status` | Финальный? | 1. Синхронно вызывает `POST .../check`, получает один из трёх кодов.
|---|---|---| 2. При `200` / `403` — сразу завершает сценарий и отвечает клиенту.
| `200` / `allow` | `allowed` | да | 3. При `203` **не ставит задачу в свою очередь анализа**; регулярно и синхронно поллит `GET .../tasks/{task_id}` до `200`/`403` или timeout (`MESSAGE_SAFETY_TASK_POLL_MAX_SEC`), затем отвечает клиенту.
| `403` / `deny` | `blocked` | да | 4. Решение «проверка быстрая или долгая» — только у `message-safety`. Ожидание poll держит **одно** клиентское HTTP-соединение; это не блокирует обработку других запросов (параллельные workers/async).
| `203` / `pending` | `pending` | нет |
Checkpoint: на время poll — запись в **`safety_tasks`** (`han_app`) для recovery при crash/timeout (I1), не очередь анализа.
Маппинг в App DB (`Message.safety_status` / `delivery_status` — см. [`arch-00-glossary.md`](arch-00-glossary.md)):
| HTTP / `message-safety` | `Message.safety_status` | `Message.delivery_status` (после завершения `POST .../messages`) | Финальный для клиента? |
|---|---|---|---|
| `200` / `allow` | `allowed` | `delivered` (после успешной отправки в Open Lines) | да |
| `403` / `deny` | `blocked` | `rejected` | да |
| `203` → затем `200`/`403` | как финальный | как финальный | да (после sync-wait) |
| timeout / circuit open | `pending` или `blocked` по политике модуля | `failed` | да (ошибка инфраструктуры) |
Circuit breaker + timeout budget (I2): при открытом circuit на `message-safety` — не слать сообщение в Bitrix; вернуть клиенту безопасную ошибку зависимости.
## api-backend ↔ bitrix-local-app (Open Lines) ## api-backend ↔ bitrix-local-app (Open Lines)
@@ -286,6 +360,38 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
`external_chat_id` всегда равен `dialog_id` приложения. `bitrix-sync` не участвует в hot path чата. `external_chat_id` всегда равен `dialog_id` приложения. `bitrix-sync` не участвует в hot path чата.
### Inbox: входящие сообщения и файлы оператора (G5)
`POST /internal/openlines/v1/inbox` — нормализованное событие от `bitrix-local-app`. Минимальный контракт MVP:
```json
{
"event_type": "message.new",
"external_chat_id": "uuid",
"bitrix_message_id": "string",
"occurred_at": "2026-07-09T12:00:00Z",
"message": {
"text": "текст оператора или пустая строка",
"files": [
{
"name": "scan.pdf",
"mime_type": "application/pdf",
"size_bytes": 12345,
"download_url": "https://..."
}
]
}
}
```
Правила:
- `event_type`: `message.new` | `dialog.closed` (и др. по OpenAPI модуля);
- idempotency по `(external_chat_id, bitrix_message_id)` на стороне `api-backend`;
- файлы оператора: api-backend скачивает по `download_url` (timeout budget) и сохраняет в **S3-data attachments** + `MessageAttachment`; MIME/size — те же продуктовые лимиты чата (`chat.attachments.*`) или отдельный allow-list модуля (зафиксировать в OpenAPI);
- пустой `text` и пустой `files` → reject события;
- детальная JSON Schema — в `bitrix-local-app/openapi.yaml` и `api-backend/openapi.yaml`.
## api-backend ↔ bitrix-sync ## api-backend ↔ bitrix-sync
**без синхронного HTTP** в пользовательских сценариях. Связь — PostgreSQL-триггеры в App DB → очередь `han_app.sync_queue` + прямой доступ `bitrix-sync` к `han_app` для write-back. `api-backend` **не создаёт** задачи синхронизации вручную. **без синхронного HTTP** в пользовательских сценариях. Связь — PostgreSQL-триггеры в App DB → очередь `han_app.sync_queue` + прямой доступ `bitrix-sync` к `han_app` для write-back. `api-backend` **не создаёт** задачи синхронизации вручную.
@@ -348,7 +454,7 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
| Selectel S3 `han-chat-quarantine` | Selectel S3 | Временное хранение вложений клиента до verdict | | Selectel S3 `han-chat-quarantine` | Selectel S3 | Временное хранение вложений клиента до verdict |
| Selectel S3 `han-chat-attachments` | Selectel S3 | Проверенные файлы чата | | Selectel S3 `han-chat-attachments` | Selectel S3 | Проверенные файлы чата |
| Selectel S3 `han-chat-documents` | Selectel S3 | Документы компании для клиента | | Selectel S3 `han-chat-documents` | Selectel S3 | Документы компании для клиента |
| Redis | `redis` | rate limits, OTP counters, coordination/realtime state | | Redis | `redis` | rate limits API (`/0`), realtime/coordination (опц. `/1`); **не** OTP counters |
## Observability-контракты ## Observability-контракты
@@ -367,8 +473,8 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
| `event_type` | `session_start` | | `event_type` | `session_start` |
| `ux_session_id` | UUID новой UX-сессии | | `ux_session_id` | UUID новой UX-сессии |
| `start_reason` | `first_launch` / `cold_start` / `idle_timeout` | | `start_reason` | `first_launch` / `cold_start` / `idle_timeout` |
| `guest_session_id` | UUID или null | | `user_id` | из JWT |
| `user_id` | null (до auth bootstrap) | | `guest_session_id` | не используется (endpoint только с JWT) |
| `request_id` | из `X-Request-ID` | | `request_id` | из `X-Request-ID` |
| `ip`, `user_agent` | из proxy headers | | `ip`, `user_agent` | из proxy headers |
@@ -376,7 +482,7 @@ Raw OTP и полный номер телефона в audit **не** пишут
### Audit: выдача download URL ### Audit: выдача download URL
При `GET /api/v1/documents/{document_id}/download-url` и аналогичных endpoint вложений чата api-backend создаёт запись: При `GET /api/v1/documents/{document_id}/download-url` и `GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url` api-backend создаёт запись:
| Поле | Пример | | Поле | Пример |
|---|---| |---|---|
@@ -17,7 +17,7 @@
- Весь backend-контур поднимается **одной командой** `docker compose up -d` из корня репозитория (`backend/`). - Весь backend-контур поднимается **одной командой** `docker compose up -d` из корня репозитория (`backend/`).
- Корневой `docker-compose.yml` — единственный источник правды для production-like среды. Отдельных compose-файлов для production-деплоя отдельных сервисов не должно быть. - Корневой `docker-compose.yml` — единственный источник правды для production-like среды. Отдельных compose-файлов для production-деплоя отдельных сервисов не должно быть.
- **Один `nginx`** поднимается из корневого `docker-compose.yml` и является единой публичной точкой входа с маршрутизацией на все сервисы: - **Один `nginx`** поднимается из корневого `docker-compose.yml` и является единой публичной точкой входа с маршрутизацией на все сервисы:
- `/api/*`, `/realtime/*``api-backend`; - `/api/*` `api-backend` (REST и `WS /api/v1/realtime`; отдельный path `/realtime/*` **не** используется);
- `/auth/*``keycloak`; - `/auth/*``keycloak`;
- `/bitrix/*` (public: `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement`, `/health/*` для `bitrix-local-app`) → `bitrix-local-app`; - `/bitrix/*` (public: `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement`, `/health/*` для `bitrix-local-app`) → `bitrix-local-app`;
- `/bitrix/sync/*` (public: webhook CRM sync для `bitrix-sync`) → `bitrix-sync`; - `/bitrix/sync/*` (public: webhook CRM sync для `bitrix-sync`) → `bitrix-sync`;
@@ -118,7 +118,7 @@ Reverse proxy и единственная публичная точка вход
- **единый домен MVP** (напр. `tohin.ru` с путями `/api/*`, `/auth/*`, web): считается веб-доменом; порт `80` — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS; - **единый домен MVP** (напр. `tohin.ru` с путями `/api/*`, `/auth/*`, web): считается веб-доменом; порт `80` — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS;
- **auth** на том же host, что API (`/auth/*`): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host); - **auth** на том же host, что API (`/auth/*`): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host);
- **Bitrix callbacks** (`/bitrix/*`, `/bitrix/sync/*`): только HTTPS; порт `80` не обслуживает эти location — только redirect; - **Bitrix callbacks** (`/bitrix/*`, `/bitrix/sync/*`): только HTTPS; порт `80` не обслуживает эти location — только redirect;
- маршрутизирует `/api/*` и `/realtime/*` в `api-backend`; - маршрутизирует `/api/*` в `api-backend` (включая WebSocket upgrade для `/api/v1/realtime`);
- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен; - маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен;
- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`; - маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`;
- маршрутизирует `/bitrix/sync/*` webhook endpoint в `bitrix-sync`; - маршрутизирует `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
@@ -127,6 +127,7 @@ Reverse proxy и единственная публичная точка вход
- **production-like / production**: отдаёт **статическую сборку Expo web** из volume или каталога (`/usr/share/nginx/html` или аналог); `index.html` + assets, SPA fallback `try_files $uri /index.html`; - **production-like / production**: отдаёт **статическую сборку Expo web** из volume или каталога (`/usr/share/nginx/html` или аналог); `index.html` + assets, SPA fallback `try_files $uri /index.html`;
- **local dev** (опционально): при `FRONTEND_DEV_PROXY_ENABLED=true` проксирует `/` на Expo dev server (`EXPO_DEV_SERVER_URL`, напр. `http://host.docker.internal:8081`); - **local dev** (опционально): при `FRONTEND_DEV_PROXY_ENABLED=true` проксирует `/` на Expo dev server (`EXPO_DEV_SERVER_URL`, напр. `http://host.docker.internal:8081`);
- передает upstream-сервисам `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`; - передает upstream-сервисам `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`;
- если входящий запрос **без** `X-Request-ID`, nginx **генерирует** UUID и устанавливает заголовок до proxy_pass (I3);
- задает разумные `proxy_connect_timeout`, `proxy_read_timeout`, `client_max_body_size`; - задает разумные `proxy_connect_timeout`, `proxy_read_timeout`, `client_max_body_size`;
- применяет edge rate limits для auth, API и download endpoints; - применяет edge rate limits для auth, API и download endpoints;
- ограничивает частоту соединений и размер тела запроса; - ограничивает частоту соединений и размер тела запроса;
@@ -188,8 +189,8 @@ Python worker/service **двусторонней** синхронизации Ap
- поддерживает graceful shutdown и rate limiting Bitrix REST; - поддерживает graceful shutdown и rate limiting Bitrix REST;
- не блокирует пользовательский API при ошибках Битрикс24; - не блокирует пользовательский API при ошибках Битрикс24;
- не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`; - не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`;
- **не участвует** в hot path чата Open Lines. - **не участвует** в hot path чата Open Lines;
- `bitrix-sync` должен быть подключаем через .env (если отключили, то синхронизация с битрикс24 не проводится; если не отключили - проводится) - включается/отключается флагом **`BITRIX_SYNC_ENABLED`** в `.env` (default `true`): при `false` сервис не стартует или работает в no-op (синхронизация с Bitrix24 CRM не выполняется).
### bitrix-local-app ### bitrix-local-app
@@ -223,29 +224,33 @@ Python worker/service **двусторонней** синхронизации Ap
### keycloak ### keycloak
Identity provider. Identity provider. **Обязателен** в compose-контуре с первого запуска.
Требования: Требования:
- отдельный realm для приложения; - отдельный realm для приложения;
- отдельный frontend client с PKCE; - отдельный frontend client с PKCE (обязателен);
- backend client для service-to-service сценариев; - confidential backend client **optional** (не используется в hot path MVP; S2S между сервисами — service tokens);
- публичный issuer должен соответствовать HTTPS URL, видимому frontend-приложению; - публичный issuer должен соответствовать HTTPS URL, видимому frontend-приложению;
- включены proxy settings для работы за `nginx`; - включены proxy settings для работы за `nginx`;
- импорт realm в local/dev; - импорт realm в local/dev;
- использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше); - использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше);
- healthcheck. - OTP mock / SMS SPI — см. arch-04;
- healthcheck;
- взаимодействия — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Frontend ↔ Keycloak», и [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Keycloak».
### redis ### redis
Очереди, кеш, rate limiting. Кэш, rate limiting, coordination (не единственное хранилище бизнес-событий).
Требования: Требования:
- не использовать как единственное надежное хранилище бизнес-событий; - не использовать как единственное надежное хранилище бизнес-событий;
- хранить счетчики API-level rate limits; - хранить счетчики API-level rate limits и idempotency keys (`api-backend`);
- поддерживать TTL для лимитных ключей; - поддерживать TTL для лимитных и idempotency ключей;
- sync_queue хранится в PostgreSQL (`han_app`), Redis может использоваться для wake-up/locking/queue optimization. - **не** хранить OTP counters для `api-backend` (OTP — зона Keycloak/SPI);
- sync_queue хранится в PostgreSQL (`han_app`), Redis может использоваться для wake-up/locking/queue optimization;
- разделение DB index (I4): см. arch-04 (`REDIS_URL`, `MESSAGE_SAFETY_REDIS_URL`).
### otel-collector ### otel-collector
@@ -261,9 +266,9 @@ Identity provider.
Рекомендуемые сети: Рекомендуемые сети:
- `public`: `nginx`, frontend dev access, внешний HTTPS entrypoint. - `public`: `nginx`, `keycloak` (для прокси `/auth/*`), frontend static/dev access, внешний HTTPS entrypoint.
- `backend`: API, `message-safety`, `bitrix-sync`, `bitrix-local-app`, `redis` (managed PostgreSQL — вне compose, в VPC). - `backend`: `api-backend`, `message-safety`, `bitrix-sync`, `bitrix-local-app`, `keycloak`, `redis` (managed PostgreSQL — вне compose, в VPC).
- `observability`: otel-collector. - `observability`: `otel-collector` + сервисы, экспортирующие telemetry.
Базы данных, Redis, Keycloak internal port и API internal port не должны публиковаться наружу. `message-safety` доступен только внутри сети `backend`. Основной пользовательский путь должен идти через `nginx` и HTTPS. Базы данных, Redis, Keycloak internal port и API internal port не должны публиковаться наружу. `message-safety` доступен только внутри сети `backend`. Основной пользовательский путь должен идти через `nginx` и HTTPS.
@@ -320,8 +325,9 @@ Identity provider.
Рекомендуемая схема: Рекомендуемая схема:
- **веб-домен** (MVP: `tohin.ru` или `app.example.ru`): `/api/*`, `/auth/*`, `/realtime/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация; - **веб-домен** (MVP: `tohin.ru` или `app.example.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация;
- **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`, `/realtime/*`; - **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`;
- для `location` WebSocket (`/api/v1/realtime`): `proxy_http_version 1.1`, `Upgrade`/`Connection` headers, увеличенный `proxy_read_timeout`;
- домен или path `/bitrix/*``bitrix-local-app`; `/bitrix/sync/*``bitrix-sync`; - домен или path `/bitrix/*``bitrix-local-app`; `/bitrix/sync/*``bitrix-sync`;
- `GET/POST /bitrix/handler` и `GET/POST /bitrix/install` доступны публично для Bitrix24; - `GET/POST /bitrix/handler` и `GET/POST /bitrix/install` доступны публично для Bitrix24;
- `/bitrix/placement` доступен публично как заглушка UI настроек коннектора; - `/bitrix/placement` доступен публично как заглушка UI настроек коннектора;
@@ -396,7 +402,7 @@ WAF не заменяет обязательные лимиты, валидац
- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake; - `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake;
- `api-backend`: HTTP 200 от `/health/ready`; - `api-backend`: HTTP 200 от `/health/ready`;
- `message-safety`: HTTP 200 от `/health/ready` (проверяет PostgreSQL, Redis, workers, read S3-quarantine); - `message-safety`: HTTP 200 от `/health/ready` (проверяет PostgreSQL, Redis, workers, read S3-quarantine);
- `bitrix-sync`: процесс жив, подключение к App DB доступно; - `bitrix-sync`: HTTP 200 от `/health/live` и `/health/ready` (ready — PostgreSQL + доступ к `sync_queue`);
- `bitrix-local-app`: HTTP 200 от `/health/live`, readiness показывает наличие OAuth-токенов после установки приложения; - `bitrix-local-app`: HTTP 200 от `/health/live`, readiness показывает наличие OAuth-токенов после установки приложения;
- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL; - `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;
- `redis`: `redis-cli ping`; - `redis`: `redis-cli ping`;
+25 -6
View File
@@ -85,7 +85,7 @@ Managed PostgreSQL **поднимается до** развёртывания п
| Группа | Ключи | | Группа | Ключи |
|---|---| |---|---|
| Auth | `auth.phone.enabled`, `auth.password.enabled` | | Auth | `auth.phone.enabled`, `auth.password.enabled` |
| OTP (продукт) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` | | OTP (продукт; потребитель — Keycloak SPI, не api-backend) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` |
| Оператор | `operator.call.phone` | | Оператор | `operator.call.phone` |
| Consent | `consent.personal_data.*`, `consent.user_agreement.*`, `consent.marketing.*` | | Consent | `consent.personal_data.*`, `consent.user_agreement.*`, `consent.marketing.*` |
| Файлы чата | `chat.attachments.*` | | Файлы чата | `chat.attachments.*` |
@@ -118,7 +118,7 @@ chat.attachments.allowed_mime_types=image/jpeg,image/png,image/webp,image/heic,i
chat.attachments.disallowed_extensions=svg,doc,docx,xls,xlsx,csv chat.attachments.disallowed_extensions=svg,doc,docx,xls,xlsx,csv
chat.attachments.max_size_mb=5 chat.attachments.max_size_mb=5
chat.attachments.storage=selectel_s3 chat.attachments.storage=selectel_s3
chat.attachments.upload_mode=backend_controlled_upload chat.attachments.upload_mode=presigned_put
chat.attachments.safety_scan_required=true chat.attachments.safety_scan_required=true
rate_limit.message_send.per_user=30/minute rate_limit.message_send.per_user=30/minute
@@ -196,9 +196,14 @@ KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=1234 KEYCLOAK_OTP_MOCK_CODE=1234
# ============================================================================= # =============================================================================
# Redis # Redis (I4: раздельные DB index)
# ============================================================================= # =============================================================================
# /0 — api-backend: rate limits, idempotency
# /1 — api-backend realtime/coordination (опционально; можно совместить с /0)
# /2 — message-safety: verdict cache / workers
REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://redis:6379/0
REDIS_REALTIME_URL=redis://redis:6379/1
MESSAGE_SAFETY_REDIS_URL=redis://redis:6379/2
# ============================================================================= # =============================================================================
# Service tokens (internal API) — все переменные только в backend/.env # Service tokens (internal API) — все переменные только в backend/.env
@@ -211,15 +216,21 @@ BITRIX_API_FORWARD_TOKEN=change-me
BITRIX_SYNC_SERVICE_TOKEN=change-me BITRIX_SYNC_SERVICE_TOKEN=change-me
# ============================================================================= # =============================================================================
# api-backend (интеграции) # api-backend (интеграции + resilience I2)
# ============================================================================= # =============================================================================
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080 BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
MESSAGE_SAFETY_URL=http://message-safety:8080 MESSAGE_SAFETY_URL=http://message-safety:8080
MESSAGE_SAFETY_CIRCUIT_FAILURE_THRESHOLD=5
MESSAGE_SAFETY_CIRCUIT_OPEN_SEC=30
BITRIX_LOCAL_APP_CIRCUIT_FAILURE_THRESHOLD=5
BITRIX_LOCAL_APP_CIRCUIT_OPEN_SEC=30
BITRIX_LOCAL_APP_HTTP_TIMEOUT_SEC=10
# ============================================================================= # =============================================================================
# bitrix-sync # bitrix-sync
# ============================================================================= # =============================================================================
BITRIX_SYNC_ENABLED=true
BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru
BITRIX_SYNC_CRM_WEBHOOK_URL=change-me BITRIX_SYNC_CRM_WEBHOOK_URL=change-me
BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60 BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60
@@ -243,6 +254,7 @@ BITRIX_APPLICATION_TOKEN=change-me
# ============================================================================= # =============================================================================
# message-safety (technical) # message-safety (technical)
# ============================================================================= # =============================================================================
# POST check timeout; poll interval/max — бюджет sync-wait внутри POST .../messages (G4)
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5 MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2 MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300 MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
@@ -280,10 +292,17 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
## Namespace переменных Bitrix ## Namespace переменных Bitrix
- `bitrix-local-app`: `BITRIX_CLIENT_*`, `BITRIX_CONNECTOR_*`, `BITRIX_PUBLIC_BASE_URL`, `BITRIX_DATABASE_URL`, `BITRIX_API_FORWARD_URL`, `BITRIX_APPLICATION_TOKEN` + service tokens. - `bitrix-local-app`: `BITRIX_CLIENT_*`, `BITRIX_CONNECTOR_*`, `BITRIX_PUBLIC_BASE_URL`, `BITRIX_DATABASE_URL`, `BITRIX_API_FORWARD_URL`, `BITRIX_APPLICATION_TOKEN` + service tokens.
- `api-backend`: `BITRIX_LOCAL_APP_BASE_URL`, `MESSAGE_SAFETY_URL` + service tokens; **бизнес-настройки** — из `app_settings`. - `api-backend`: `BITRIX_LOCAL_APP_BASE_URL`, `MESSAGE_SAFETY_URL`, circuit/timeout vars, Redis `/0`/`/1` + service tokens; **бизнес-настройки** — из `app_settings`. OTP counters **не** ведёт.
- `bitrix-sync`: `BITRIX_SYNC_*`, `BITRIX_SYNC_WEBHOOK_TOKEN`, `BITRIX_SYNC_SERVICE_TOKEN`. - `bitrix-sync`: `BITRIX_SYNC_ENABLED`, `BITRIX_SYNC_*`, `BITRIX_SYNC_WEBHOOK_TOKEN`, `BITRIX_SYNC_SERVICE_TOKEN`.
- `bitrix-sync` не читает `BITRIX_CLIENT_ID` / `BITRIX_CLIENT_SECRET`. - `bitrix-sync` не читает `BITRIX_CLIENT_ID` / `BITRIX_CLIENT_SECRET`.
### `BITRIX_SYNC_ENABLED`
| Значение | Поведение |
|---|---|
| `true` (default) | `bitrix-sync` обрабатывает `sync_queue` и принимает CRM webhook |
| `false` | синхронизация с Bitrix24 CRM не выполняется (сервис не стартует или no-op); чат Open Lines через `bitrix-local-app` **не** затрагивается |
## Разрешённые типы файлов чата (MVP) ## Разрешённые типы файлов чата (MVP)
Источник значений — ключи **`app_settings`** (раздел «Seed MVP»). Остальные arch-* **ссылаются сюда**. Источник значений — ключи **`app_settings`** (раздел «Seed MVP»). Остальные arch-* **ссылаются сюда**.
@@ -26,7 +26,8 @@
- Исключения допускаются только для аудита, админки, технического восстановления и миграций. - Исключения допускаются только для аудита, админки, технического восстановления и миграций.
- Прикладные сущности, имеют `id`, `created_at`, `updated_at`, `updater_user_id`. - Прикладные сущности, имеют `id`, `created_at`, `updated_at`, `updater_user_id`.
- Системные таблицы (`app_settings`, `text_resources`, `popular_questions`, `sync_queue`, audit, справочники) могут использовать `user_id = NULL` или отдельное поле `actor_type` — по спецификации модуля `database`. - Системные таблицы (`app_settings`, `text_resources`, `popular_questions`, `sync_queue`, audit, справочники) могут использовать `user_id = NULL` или отдельное поле `actor_type` — по спецификации модуля `database`.
- Для списков использовать справочники. Если значения в столбце могут принимать определенный набор значений, записывать их через ИД (sequence) и создавать справочник с расшифровкой ИД. Это существенно позволит экономить на размере таблиц. - Для **строковых enum из arch-00** (`Dialog.status`, `Message.safety_status`, `Message.delivery_status`, `sender_type`, `scan_status` и т.п.) справочник sequence **не** обязателен: значения фиксированы контрактом API.
- Для больших/изменяемых списков (типы документов post-MVP, причины, классификаторы UI) — справочники с ID (sequence) и расшифровкой.
- Для часто используемых фильтров добавляются индексы. - Для часто используемых фильтров добавляются индексы.
- Миграции не должны удалять данные без отдельного согласования. - Миграции не должны удалять данные без отдельного согласования.
- Все юзеры должны иметь ИД, которое указывается в `updater_user_id` которое они меняют. - Все юзеры должны иметь ИД, которое указывается в `updater_user_id` которое они меняют.
@@ -67,11 +68,11 @@ Raw OTP запрещено хранить в открытом виде: это
- idempotency, если операция может повториться; - idempotency, если операция может повториться;
- отсутствие секретов и PII в логах. - отсутствие секретов и PII в логах.
Дополнительно: Дополнительно для модулей с async/worker:
- идемпотентность обработки задач очереди;
- поведение при повторной доставке webhook;
## Definition of Done - таймауты и retry/backoff.## Definition of Done
Модуль считается готовым, если: Модуль считается готовым, если: