Правки от GPT
This commit is contained in:
@@ -8,25 +8,25 @@
|
|||||||
|
|
||||||
| Документ | Содержание |
|
| Документ | Содержание |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`arch-00-glossary.md`](arch-00-glossary.md) | Канонические имена: сущности, поля, id, enum, бакеты S3, env |
|
| [`arch-00-glossary.md`](arch-00-glossary.md) | Канонические имена и семантика enum/lifecycle: сущности, поля, id, enum, бакеты S3, env |
|
||||||
| [`arch-01-system-architecture.md`](arch-01-system-architecture.md) | Общая архитектура: компоненты, сценарии, потоки данных, безопасность |
|
| [`arch-01-system-architecture.md`](arch-01-system-architecture.md) | Общая архитектура: компоненты, сценарии, потоки данных, безопасность |
|
||||||
| [`arch-02-api-contracts.md`](arch-02-api-contracts.md) | Реестр API-контрактов, realtime, гостевая сессия, OpenAPI, аудит |
|
| [`arch-02-api-contracts.md`](arch-02-api-contracts.md) | Реестр API-контрактов, realtime, гостевая сессия, OpenAPI, аудит |
|
||||||
| [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md) | Требования к Docker Compose, nginx, сетям, TLS и rate limits |
|
| [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md) | Требования к Docker Compose, nginx, сетям, TLS и rate limits |
|
||||||
| [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md) | `.env` (infra), таблица `app_settings`, service tokens, типы файлов |
|
| [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md) | `.env` (infra), таблица `app_settings`, значения service-token переменных, типы файлов |
|
||||||
| [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md) | Правила разработки модулей отдельными агентами |
|
| [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md) | Правила разработки модулей отдельными агентами |
|
||||||
|
|
||||||
## Как читать
|
## Как читать
|
||||||
|
|
||||||
1. Начните с **arch-01** — общая картина и зафиксированные решения MVP.
|
1. Начните с **arch-01** — общая картина и зафиксированные решения MVP.
|
||||||
2. При работе с API — **arch-02**; при деплое — **arch-03**; при настройках — **arch-04**.
|
2. При работе с API — **arch-02**; при деплое — **arch-03**; при настройках — **arch-04**.
|
||||||
3. Спорные **имена** полей, id, enum, бакетов — **arch-00** (не правила и не лимиты).
|
3. Спорные **имена** полей, id, enum, бакетов и базовая семантика enum/lifecycle — **arch-00**. Лимиты и правила реализации остаются в профильных arch-*.
|
||||||
4. Перед разработкой модуля — **arch-05** и релевантные разделы arch-01/arch-02.
|
4. Перед разработкой модуля — **arch-05** и релевантные разделы arch-01/arch-02.
|
||||||
|
|
||||||
## Приоритет документов
|
## Приоритет документов
|
||||||
|
|
||||||
При конфликте требований:
|
При конфликте требований:
|
||||||
|
|
||||||
1. **arch-00** — только **имена** (поля, id, enum, бакеты, env); не правила и не лимиты.
|
1. **arch-00** — **имена и базовая семантика** (поля, id, enum, бакеты, env, смысл статусов); не бизнес-лимиты и не детальная реализация.
|
||||||
2. **arch-01** — границы сервисов, сценарии, sync, безопасность.
|
2. **arch-01** — границы сервисов, сценарии, sync, безопасность.
|
||||||
3. **arch-02** — HTTP-контракты и направление вызовов.
|
3. **arch-02** — HTTP-контракты и направление вызовов.
|
||||||
4. **arch-03** — инфраструктура и nginx.
|
4. **arch-03** — инфраструктура и nginx.
|
||||||
@@ -56,6 +56,11 @@
|
|||||||
| # | Пробел | Статус |
|
| # | Пробел | Статус |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| G8 | Явный список `is_public=true` для ключей `app_settings` | Отложить до оформления сервисов; seed в модуле `database` |
|
| G8 | Явный список `is_public=true` для ключей `app_settings` | Отложить до оформления сервисов; seed в модуле `database` |
|
||||||
|
| G9 | GRANT-модель `bitrix_sync_user` на `han_app`: таблицы, колонки, read/write границы | Уточнить в спецификации `database` и `bitrix-sync` |
|
||||||
|
| G10 | Полный DTO `GET /api/v1/public/app-config` и мэппинг `setting_key → response field` | Уточнить при оформлении OpenAPI `api-backend` |
|
||||||
|
| G11 | Версионирование API/WS: deprecation policy, срок поддержки v1, `ws_protocol_version` | Уточнить перед публичным релизом API |
|
||||||
|
| G12 | Масштабирование realtime: Redis Pub/Sub, sticky sessions, backpressure при нескольких репликах `api-backend` | Post-MVP / перед горизонтальным масштабированием |
|
||||||
|
| G13 | Contract tests между `api-backend`, `message-safety`, `bitrix-local-app`, `bitrix-sync` | Добавить в DoD модулей после появления OpenAPI |
|
||||||
|
|
||||||
## Обновление документации
|
## Обновление документации
|
||||||
|
|
||||||
|
|||||||
@@ -75,9 +75,12 @@
|
|||||||
|
|
||||||
### Когда **та же** `UxSession` продолжается
|
### Когда **та же** `UxSession` продолжается
|
||||||
- возврат из фона **в пределах** idle timeout (напр. через 5 минут — **без** нового `session_start`);
|
- возврат из фона **в пределах** idle timeout (напр. через 5 минут — **без** нового `session_start`);
|
||||||
- успешный OTP или refresh access token;
|
- успешный refresh access token, если `ux_session_id` уже создан и idle timeout не превышен;
|
||||||
|
- успешный OTP внутри уже активной UX-сессии, если повторная авторизация не очистила память приложения;
|
||||||
- навигация между экранами внутри приложения.
|
- навигация между экранами внутри приложения.
|
||||||
|
|
||||||
|
При первом JWT-входе после гостевого режима или после cold start, когда в памяти нет активного `ux_session_id`, frontend создаёт новую UX-сессию через `POST /api/v1/analytics/session-start`.
|
||||||
|
|
||||||
## `Dialog.status`
|
## `Dialog.status`
|
||||||
|
|
||||||
| Значение | Смысл |
|
| Значение | Смысл |
|
||||||
@@ -136,6 +139,7 @@ Realtime-событие `message.status` передаёт актуальные `
|
|||||||
| `safety` | `message-safety` |
|
| `safety` | `message-safety` |
|
||||||
| `openlines` | `bitrix-local-app`, приёмник inbox на `api-backend` |
|
| `openlines` | `bitrix-local-app`, приёмник inbox на `api-backend` |
|
||||||
| `sync` | `bitrix-sync` |
|
| `sync` | `bitrix-sync` |
|
||||||
|
| `settings` | internal settings bridge на `api-backend` для Keycloak SPI |
|
||||||
|
|
||||||
## Bitrix24 Open Lines
|
## Bitrix24 Open Lines
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ HAN Chat - приложение для мигрантов, где стартов
|
|||||||
## Пользовательские сценарии
|
## Пользовательские сценарии
|
||||||
|
|
||||||
1. Клиент открывает мобильное или web-приложение и видит главный экран с приветствием, популярными вопросами и полем ввода.
|
1. Клиент открывает мобильное или web-приложение и видит главный экран с приветствием, популярными вопросами и полем ввода.
|
||||||
2. Frontend определяет, нужна ли **новая UX-сессия**, и при необходимости отправляет событие **`session_start`** (см. «Аналитическая UX-сессия»). Клиент может изучить сервис без авторизации.
|
2. Frontend определяет, нужна ли **новая UX-сессия**, но до JWT не вызывает backend write-endpoint: клиент может изучить сервис без авторизации через guest UI и `GET /api/v1/public/*`.
|
||||||
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. После успешной авторизации frontend с JWT вызывает **`POST /auth/bootstrap`** (в теле — принятые согласия): api-backend создаёт или находит локального пользователя по `keycloak_sub`, сохраняет согласия на `user_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. Затем при необходимости — `POST /analytics/session-start`.
|
6. После успешной авторизации frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** (в теле — принятые согласия): api-backend создаёт или находит локального пользователя по `keycloak_sub`, сохраняет согласия на `user_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. Затем, если у frontend нет активной UX-сессии или она истекла, вызывается **`POST /api/v1/analytics/session-start`**.
|
||||||
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 и отображается в чате приложения.
|
||||||
@@ -223,7 +223,7 @@ 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`;
|
- продуктовые лимиты OTP (`otp.phone.*` из `app_settings`) через authenticator/SPI и settings bridge `api-backend` (см. arch-04); счётчики попыток — в зоне Keycloak (Redis DB Keycloak/SPI или in-memory Keycloak), **не** в `api-backend`;
|
||||||
- хранение учетных записей;
|
- хранение учетных записей;
|
||||||
- выдачу и обновление токенов (access + refresh);
|
- выдачу и обновление токенов (access + refresh);
|
||||||
- настройку realm, clients, roles, policies;
|
- настройку realm, clients, roles, policies;
|
||||||
@@ -294,7 +294,7 @@ api-backend не решает, sync или async нужна проверка в
|
|||||||
|
|
||||||
- UI главного экрана, популярные вопросы и публичный контент — через `GET /api/v1/public/*` (без JWT);
|
- UI главного экрана, популярные вопросы и публичный контент — через `GET /api/v1/public/*` (без JWT);
|
||||||
- pop-up согласий показывается **до** OTP, но факт принятия хранится **только на клиенте** до получения tokens;
|
- pop-up согласий показывается **до** OTP, но факт принятия хранится **только на клиенте** до получения tokens;
|
||||||
- **`POST /consents`**, **`POST /analytics/session-start`** и остальные write/API чата — **только с JWT**;
|
- **`POST /api/v1/consents`**, **`POST /api/v1/analytics/session-start`** и остальные write/API чата — **только с JWT**;
|
||||||
- опциональный локальный `guest_session_id` (UUID в secure storage) может использоваться frontend для своей аналитики/идемпотентности UI, но **не** является auth и **не** открывает backend write-endpoint.
|
- опциональный локальный `guest_session_id` (UUID в secure storage) может использоваться frontend для своей аналитики/идемпотентности UI, но **не** является auth и **не** открывает backend write-endpoint.
|
||||||
|
|
||||||
## Аналитическая UX-сессия (`ux_session_id`)
|
## Аналитическая UX-сессия (`ux_session_id`)
|
||||||
@@ -326,7 +326,7 @@ api-backend не решает, sync или async нужна проверка в
|
|||||||
|
|
||||||
1. Клиент открывает приложение. Пока нет JWT — гостевой UI; `session-start` не вызывается.
|
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 не показывается**, затем при необходимости `POST /analytics/session-start`.
|
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**, затем при необходимости начала новой UX-сессии вызывает `POST /api/v1/analytics/session-start`.
|
||||||
4. Frontend работает как авторизованный пользователь (история, профиль, чат).
|
4. Frontend работает как авторизованный пользователь (история, профиль, чат).
|
||||||
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
|
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@ api-backend не решает, sync или async нужна проверка в
|
|||||||
**Общая ветка вердикта (оба типа):**
|
**Общая ветка вердикта (оба типа):**
|
||||||
|
|
||||||
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
|
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
|
||||||
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=delivered`), отправляет в Bitrix24, подтверждает клиенту; `Dialog.status` → `waiting_for_company`.
|
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=accepted`) и фиксирует задачу доставки в Open Lines. После успешной отправки через `bitrix-local-app` статус становится `delivery_status=delivered`, API подтверждает клиенту финальный результат; `Dialog.status` → `waiting_for_company`. Если Bitrix24/S3/dependency недоступны после allow, статус становится `delivery_status=failed`, клиент получает безопасную ошибку зависимости.
|
||||||
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 нет.
|
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 нет.
|
||||||
- финальный **`200 allow`** → как п. 6, затем ответ клиенту;
|
- финальный **`200 allow`** → как п. 6, затем ответ клиенту;
|
||||||
- финальный **`403 deny`** → как п. 5, затем ответ клиенту;
|
- финальный **`403 deny`** → как п. 5, затем ответ клиенту;
|
||||||
@@ -425,15 +425,22 @@ api-backend не решает, sync или async нужна проверка в
|
|||||||
|
|
||||||
Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
|
Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
|
||||||
|
|
||||||
|
### Надёжность доставки и recovery
|
||||||
|
|
||||||
|
- Для доставки в Bitrix24 используется transactional outbox/checkpoint в App DB: запись `Message` и запись намерения доставки фиксируются атомарно, а повторная отправка в `bitrix-local-app` идемпотентна по `message_id` / `Idempotency-Key`.
|
||||||
|
- `delivery_status=accepted` означает, что API принял сообщение и завершил safety allow, но ещё не получил подтверждение доставки в Open Lines. `delivery_status=delivered` выставляется только после успешного ответа `bitrix-local-app` о приёме сообщения для Bitrix24 Open Lines.
|
||||||
|
- Recovery по `han_app.safety_tasks` восстанавливает только сценарии, где Message Safety вернул `203 pending` и клиентский запрос оборвался из-за timeout/crash. Recovery job повторно опрашивает `message-safety` по `task_id`, затем идемпотентно выполняет promote/delete quarantine и обновляет `Message`/`MessageAttachment`.
|
||||||
|
- Объекты в S3-quarantine не удаляются при timeout safety до финального verdict; orphan-cleanup удаляет только просроченные объекты без активного `safety_tasks` или attachment metadata.
|
||||||
|
|
||||||
## Поток работы с чатом: Битрикс24 -> клиент
|
## Поток работы с чатом: Битрикс24 -> клиент
|
||||||
|
|
||||||
1. Оператор отвечает клиенту в Битрикс24 Open Lines.
|
1. Оператор отвечает клиенту в Битрикс24 Open Lines.
|
||||||
2. Битрикс24 отправляет `ONIMCONNECTOR*` webhook/event в `bitrix-local-app`.
|
2. Битрикс24 отправляет `ONIMCONNECTOR*` webhook/event в `bitrix-local-app`.
|
||||||
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` включен. При недоступности API событие остаётся во внутреннем inbox, повторяется с backoff и после исчерпания retry попадает в DLQ; дубликаты определяются по `(external_chat_id, bitrix_message_id)`.
|
||||||
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`, `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).
|
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` после успешного сохранения события в api-backend или идемпотентного duplicate-ack.
|
||||||
8. api-backend публикует событие для frontend через **WebSocket** (`WS /api/v1/realtime`). Если 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`.
|
||||||
@@ -517,7 +524,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
|||||||
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `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-02-api-contracts.md`](arch-02-api-contracts.md), «Service tokens (internal API)»; значения переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
|
||||||
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
|
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
|
||||||
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check` → при `203` api-backend синхронно поллит `task_id` до финального `200`/`403` (или timeout); без очереди анализа на api-backend.
|
- Входящие сообщения пользователя: синхронный `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`.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
| `BITRIX_API_INBOX_TOKEN` | `api-backend` | `bitrix-local-app` | `POST /internal/openlines/v1/inbox` | `Authorization: Bearer` |
|
| `BITRIX_API_INBOX_TOKEN` | `api-backend` | `bitrix-local-app` | `POST /internal/openlines/v1/inbox` | `Authorization: Bearer` |
|
||||||
| `BITRIX_API_FORWARD_TOKEN` | — | `bitrix-local-app` (исходящий) | то же | `Authorization: Bearer` |
|
| `BITRIX_API_FORWARD_TOKEN` | — | `bitrix-local-app` (исходящий) | то же | `Authorization: Bearer` |
|
||||||
| `BITRIX_SYNC_SERVICE_TOKEN` | `bitrix-sync` | ops / мониторинг | `GET /internal/sync/v1/*` | `Authorization: Bearer` или `X-Service-Token` |
|
| `BITRIX_SYNC_SERVICE_TOKEN` | `bitrix-sync` | ops / мониторинг | `GET /internal/sync/v1/*` | `Authorization: Bearer` или `X-Service-Token` |
|
||||||
|
| `KEYCLOAK_SETTINGS_BRIDGE_TOKEN` | `api-backend` | Keycloak SPI | `GET /internal/settings/v1/otp` | `Authorization: Bearer` |
|
||||||
|
|
||||||
Пары значений (должны совпадать):
|
Пары значений (должны совпадать):
|
||||||
|
|
||||||
@@ -79,9 +80,35 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Каталог публичных ошибок MVP
|
||||||
|
|
||||||
|
Все ошибки возвращаются в envelope выше. `details` не содержит PII, raw OTP, presigned URL и внутренние stack traces.
|
||||||
|
|
||||||
|
| HTTP | `error.code` | Когда | Retry |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `400` | `validation_error` | Невалидное тело, query или header | нет |
|
||||||
|
| `400` | `phone_claim_missing` | В JWT нет канонического phone claim для `bootstrap` | нет |
|
||||||
|
| `400` | `mixed_content_not_allowed` | В сообщении одновременно текст и вложение | нет |
|
||||||
|
| `400` | `empty_message` | Нет текста и `attachment_id` | нет |
|
||||||
|
| `400` | `too_many_attachments` | Более одного вложения в MVP | нет |
|
||||||
|
| `400` | `attachment_not_completed` | `POST .../messages` с незавершённым upload | да, после `complete` |
|
||||||
|
| `400` | `attachment_checksum_mismatch` | Checksum клиента не совпал с объектом в S3 | нет |
|
||||||
|
| `401` | `unauthorized` | Нет access token или он невалиден | после auth |
|
||||||
|
| `401` | `token_expired` | Access token истёк | да, после refresh token grant |
|
||||||
|
| `403` | `consents_required` | Обязательные согласия не приняты | нет |
|
||||||
|
| `403` | `forbidden` | Доступ запрещён и ресурс не скрывается | нет |
|
||||||
|
| `404` | `not_found` | Ресурс не существует или принадлежит другому пользователю | нет |
|
||||||
|
| `409` | `idempotency_key_reused` | Тот же `Idempotency-Key` с другим fingerprint | нет |
|
||||||
|
| `422` | `message_blocked` | Message Safety вернул final deny | нет |
|
||||||
|
| `429` | `rate_limit_exceeded` | Edge/API лимит превышен; должен быть `Retry-After`, если повтор допустим | да |
|
||||||
|
| `503` | `dependency_unavailable` | Circuit open или недоступны safety/Bitrix/S3 | да |
|
||||||
|
| `504` | `dependency_timeout` | Истёк timeout budget внешней зависимости | да |
|
||||||
|
|
||||||
|
Правило доступа к пользовательским ресурсам: для `dialog_id`, `message_id`, `attachment_id`, `document_id`, принадлежащих другому `user_id`, api-backend по умолчанию возвращает `404 not_found`, чтобы не раскрывать существование ресурса. `403 forbidden` используется только для операций, где сам факт ресурса уже известен пользователю или оператору.
|
||||||
|
|
||||||
### `POST /api/v1/auth/bootstrap` (после OTP)
|
### `POST /api/v1/auth/bootstrap` (после OTP)
|
||||||
|
|
||||||
Вызывается **один раз** после успешного OTP и получения JWT. Создаёт локального пользователя и **сразу** сохраняет согласия из тела (атомарно в одной транзакции). **Не** создаёт UX-сессию — для этого `POST /analytics/session-start`.
|
Вызывается **один раз** после успешного OTP и получения JWT. Создаёт локального пользователя и **сразу** сохраняет согласия из тела (атомарно в одной транзакции). **Не** создаёт UX-сессию — для этого используется `POST /api/v1/analytics/session-start`.
|
||||||
|
|
||||||
**Заголовки:** `Authorization: Bearer <access_token>` — **единственный** источник идентичности пользователя.
|
**Заголовки:** `Authorization: Bearer <access_token>` — **единственный** источник идентичности пользователя.
|
||||||
|
|
||||||
@@ -113,7 +140,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Порядок после OTP:** `POST /auth/bootstrap` → `POST /analytics/session-start` → чат.
|
**Порядок после OTP:** `POST /api/v1/auth/bootstrap` → `POST /api/v1/analytics/session-start` (если нужна новая UX-сессия) → чат.
|
||||||
|
|
||||||
**Ответ `200`:** `{ "user_id": "uuid", "profile_ready": true }`.
|
**Ответ `200`:** `{ "user_id": "uuid", "profile_ready": true }`.
|
||||||
|
|
||||||
@@ -175,7 +202,9 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
|
|||||||
|
|
||||||
### Создание диалога
|
### Создание диалога
|
||||||
|
|
||||||
- `POST /api/v1/dialogs` — заголовок **`Idempotency-Key`** (обязателен); ответ `{ "dialog_id": "uuid", "status": "open" | "waiting_for_company" | "waiting_for_client" }`.
|
- `POST /api/v1/dialogs` — заголовок **`Idempotency-Key`** (обязателен); без заголовка → `400 validation_error`.
|
||||||
|
- Ответ `201` — создан новый активный диалог: `{ "dialog_id": "uuid", "status": "open" }`.
|
||||||
|
- Ответ `200` — у пользователя уже есть активный диалог или повторён тот же idempotent-запрос: `{ "dialog_id": "uuid", "status": "open" | "waiting_for_company" | "waiting_for_client" }`.
|
||||||
- **Один активный диалог** на пользователя: если уже есть диалог со статусом не `closed`, endpoint возвращает его (idempotent), новый не создаёт.
|
- **Один активный диалог** на пользователя: если уже есть диалог со статусом не `closed`, endpoint возвращает его (idempotent), новый не создаёт.
|
||||||
- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth), если у клиента ещё нет `dialog_id`.
|
- Обязателен перед первым `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)).
|
||||||
@@ -190,6 +219,7 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
|
|||||||
| TTL | **24 часа** |
|
| TTL | **24 часа** |
|
||||||
| Повтор с тем же ключом и тем же телом | тот же HTTP-ответ, без повторного side-effect |
|
| Повтор с тем же ключом и тем же телом | тот же HTTP-ответ, без повторного side-effect |
|
||||||
| Повтор с тем же ключом и **другим** телом | **`409`** `idempotency_key_reused` |
|
| Повтор с тем же ключом и **другим** телом | **`409`** `idempotency_key_reused` |
|
||||||
|
| Отсутствует на обязательном endpoint | **`400`** `validation_error` |
|
||||||
|
|
||||||
### Формат исходящего сообщения клиента (MVP)
|
### Формат исходящего сообщения клиента (MVP)
|
||||||
|
|
||||||
@@ -207,6 +237,53 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
|
|||||||
|
|
||||||
Post-MVP: допускается «текст + файлы» отдельной версией API.
|
Post-MVP: допускается «текст + файлы» отдельной версией API.
|
||||||
|
|
||||||
|
### DTO чата и profile API MVP
|
||||||
|
|
||||||
|
`MessageResponse` — общий DTO для REST и WS:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "uuid",
|
||||||
|
"dialog_id": "uuid",
|
||||||
|
"sender_type": "client",
|
||||||
|
"content_kind": "text",
|
||||||
|
"text": "Здравствуйте",
|
||||||
|
"attachments": [],
|
||||||
|
"safety_status": "allowed",
|
||||||
|
"delivery_status": "delivered",
|
||||||
|
"created_at": "2026-07-09T12:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /api/v1/dialogs` возвращает `{ "items": [DialogSummary], "next_cursor": "opaque-or-null" }`, сортировка — по `updated_at desc`. `GET /api/v1/dialogs/{dialog_id}/messages?after=<cursor>&limit=50` возвращает `{ "items": [MessageResponse], "next_cursor": "opaque-or-null" }`, сортировка — по `created_at asc` для удобства append в чате. Cursor opaque; frontend не парсит его.
|
||||||
|
|
||||||
|
`GET /api/v1/me` возвращает блочный профиль:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "uuid",
|
||||||
|
"profile": {
|
||||||
|
"personal_data": {
|
||||||
|
"full_name": "string-or-null",
|
||||||
|
"citizenship": "string-or-null",
|
||||||
|
"russian_phone": "string-or-null",
|
||||||
|
"foreign_phone": "string-or-null",
|
||||||
|
"email": "string-or-null"
|
||||||
|
},
|
||||||
|
"documents": { "count": 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`POST /api/v1/dialogs/{dialog_id}/messages`:
|
||||||
|
|
||||||
|
- заголовок `Idempotency-Key` обязателен;
|
||||||
|
- request body для текста: `{ "content_kind": "text", "text": "..." }`;
|
||||||
|
- request body для файла: `{ "content_kind": "file", "attachment_id": "uuid", "checksum": "sha256:..." }`;
|
||||||
|
- success `201`: `MessageResponse` с финальным `delivery_status=delivered`;
|
||||||
|
- safety deny: `422 message_blocked`, при этом запись может сохраняться с `safety_status=blocked`, `delivery_status=rejected`;
|
||||||
|
- dependency error: `503 dependency_unavailable` или `504 dependency_timeout`, `delivery_status=failed` если сообщение уже было создано.
|
||||||
|
|
||||||
### Загрузка вложения (MVP)
|
### Загрузка вложения (MVP)
|
||||||
|
|
||||||
Байты файла идут **напрямую в S3-quarantine** по короткоживущему **presigned URL**. `api-backend` не проксирует тело файла: выдаёт URL, проверяет результат, управляет lifecycle (promote/delete).
|
Байты файла идут **напрямую в S3-quarantine** по короткоживущему **presigned URL**. `api-backend` не проксирует тело файла: выдаёт URL, проверяет результат, управляет lifecycle (promote/delete).
|
||||||
@@ -270,6 +347,16 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
|
|||||||
- internal API версионируется тем же правилом (`/internal/{mnemonic}/v2/...`);
|
- internal API версионируется тем же правилом (`/internal/{mnemonic}/v2/...`);
|
||||||
- OpenAPI генерируется или поддерживается вручную — на усмотрение модуля, но файл обязателен в DoD (arch-05).
|
- OpenAPI генерируется или поддерживается вручную — на усмотрение модуля, но файл обязателен в DoD (arch-05).
|
||||||
|
|
||||||
|
## Keycloak SPI ↔ api-backend settings bridge
|
||||||
|
|
||||||
|
Keycloak SPI получает product limits OTP из `app_settings` через internal endpoint, а не через прямой доступ к `han_app`.
|
||||||
|
|
||||||
|
| Контракт | Владелец | Потребитель | Назначение | Защита |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `GET /internal/settings/v1/otp` | `api-backend` | Keycloak SPI | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts`, cache metadata | internal network + Bearer `KEYCLOAK_SETTINGS_BRIDGE_TOKEN` |
|
||||||
|
|
||||||
|
Ответ не содержит секретов и PII. При недоступности endpoint Keycloak SPI использует последнее валидное cached value; если cache пустой — fail-closed для выдачи OTP.
|
||||||
|
|
||||||
## Frontend ↔ Keycloak
|
## Frontend ↔ Keycloak
|
||||||
|
|
||||||
Keycloak **обязателен** в production-like контуре с первого запуска (OTP, tokens, JWKS).
|
Keycloak **обязателен** в production-like контуре с первого запуска (OTP, tokens, JWKS).
|
||||||
@@ -335,17 +422,34 @@ HTTP-семантика от `message-safety`: `200 allow`, `403 deny`, `203 pen
|
|||||||
|
|
||||||
Checkpoint: на время poll — запись в **`safety_tasks`** (`han_app`) для recovery при crash/timeout (I1), не очередь анализа.
|
Checkpoint: на время poll — запись в **`safety_tasks`** (`han_app`) для recovery при crash/timeout (I1), не очередь анализа.
|
||||||
|
|
||||||
|
Recovery contract для `han_app.safety_tasks`:
|
||||||
|
|
||||||
|
- запись создаётся, когда `message-safety` вернул `203 pending`, и содержит `task_id`, `message_id`, `attachment_id`, текущий `quarantine_object_key`, deadline и retry metadata;
|
||||||
|
- если клиентское HTTP-соединение оборвалось или api-backend упал, recovery job продолжает poll `GET /internal/safety/v1/messages/tasks/{task_id}`;
|
||||||
|
- final allow выполняет idempotent promote quarantine → S3-data и продолжает delivery checkpoint в Open Lines;
|
||||||
|
- final deny выполняет idempotent delete quarantine и выставляет `safety_status=blocked`, `delivery_status=rejected`;
|
||||||
|
- timeout/circuit после recovery budget выставляет `delivery_status=failed`, оставляет audit trail и отдаёт объект на quarantine cleanup policy;
|
||||||
|
- recovery job не принимает новых сообщений и не решает, sync или async нужна проверка: это остаётся ответственностью `message-safety`.
|
||||||
|
|
||||||
Маппинг в App DB (`Message.safety_status` / `delivery_status` — см. [`arch-00-glossary.md`](arch-00-glossary.md)):
|
Маппинг в 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`) | Финальный для клиента? |
|
| HTTP / `message-safety` | `Message.safety_status` | `Message.delivery_status` (после завершения `POST .../messages`) | Финальный для клиента? |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `200` / `allow` | `allowed` | `delivered` (после успешной отправки в Open Lines) | да |
|
| `200` / `allow` | `allowed` | `accepted` до вызова Open Lines; `delivered` только после успешной отправки в Open Lines | да |
|
||||||
| `403` / `deny` | `blocked` | `rejected` | да |
|
| `403` / `deny` | `blocked` | `rejected` | да |
|
||||||
| `203` → затем `200`/`403` | как финальный | как финальный | да (после sync-wait) |
|
| `203` → затем `200`/`403` | как финальный | как финальный | да (после sync-wait) |
|
||||||
| timeout / circuit open | `pending` или `blocked` по политике модуля | `failed` | да (ошибка инфраструктуры) |
|
| timeout / circuit open | `pending` или `blocked` по политике модуля | `failed` | да (ошибка инфраструктуры) |
|
||||||
|
|
||||||
Circuit breaker + timeout budget (I2): при открытом circuit на `message-safety` — не слать сообщение в Bitrix; вернуть клиенту безопасную ошибку зависимости.
|
Circuit breaker + timeout budget (I2): при открытом circuit на `message-safety` — не слать сообщение в Bitrix; вернуть клиенту безопасную ошибку зависимости.
|
||||||
|
|
||||||
|
Доставка в Open Lines:
|
||||||
|
|
||||||
|
- api-backend сохраняет `Message` и delivery checkpoint/outbox запись в одной транзакции после финального safety `allow`;
|
||||||
|
- `delivery_status=accepted` не считается доставкой оператору и может быть виден только как промежуточный статус в логах/recovery;
|
||||||
|
- `delivery_status=delivered` выставляется после успешного ответа `POST /internal/openlines/v1/messages`;
|
||||||
|
- повтор delivery checkpoint идемпотентен по `message_id` и не создаёт дубль в Bitrix24;
|
||||||
|
- если `bitrix-local-app` или Bitrix24 недоступны после allow, `delivery_status=failed`, клиент получает dependency error, а recovery может повторить доставку только если контракт модуля явно разрешает безопасный retry без дубля.
|
||||||
|
|
||||||
## api-backend ↔ bitrix-local-app (Open Lines)
|
## api-backend ↔ bitrix-local-app (Open Lines)
|
||||||
|
|
||||||
Мнемоника сервиса: **`openlines`**. Endpoint Open Lines на стороне `bitrix-local-app` и приёмник событий на стороне `api-backend` используют один префикс `/internal/openlines/v1/`.
|
Мнемоника сервиса: **`openlines`**. Endpoint Open Lines на стороне `bitrix-local-app` и приёмник событий на стороне `api-backend` используют один префикс `/internal/openlines/v1/`.
|
||||||
@@ -387,8 +491,11 @@ Circuit breaker + timeout budget (I2): при открытом circuit на `mes
|
|||||||
Правила:
|
Правила:
|
||||||
|
|
||||||
- `event_type`: `message.new` | `dialog.closed` (и др. по OpenAPI модуля);
|
- `event_type`: `message.new` | `dialog.closed` (и др. по OpenAPI модуля);
|
||||||
- idempotency по `(external_chat_id, bitrix_message_id)` на стороне `api-backend`;
|
- idempotency по `(external_chat_id, bitrix_message_id)` на стороне `api-backend`; повтор того же события возвращает `200`/`204` без повторного side-effect;
|
||||||
- файлы оператора: api-backend скачивает по `download_url` (timeout budget) и сохраняет в **S3-data attachments** + `MessageAttachment`; MIME/size — те же продуктовые лимиты чата (`chat.attachments.*`) или отдельный allow-list модуля (зафиксировать в OpenAPI);
|
- если `api-backend` недоступен, `bitrix-local-app` хранит событие во внутреннем inbox, повторяет forward с exponential backoff и после исчерпания retry переводит запись в DLQ со статусом `dead_letter`;
|
||||||
|
- `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery` только после успешного ответа `api-backend` или после идемпотентного duplicate-ack;
|
||||||
|
- файлы оператора: api-backend скачивает по `download_url` (timeout budget) и сохраняет в **S3-data attachments** + `MessageAttachment`; в MVP применяются те же продуктовые лимиты `chat.attachments.allowed_*` и `chat.attachments.max_size_mb`, что и для клиентских файлов;
|
||||||
|
- сообщения и файлы оператора считаются доверенным Bitrix24-channel для Message Safety: они не проходят outbound moderation pipeline, но проходят MIME/size validation, antivirus policy модуля и audit скачивания;
|
||||||
- пустой `text` и пустой `files` → reject события;
|
- пустой `text` и пустой `files` → reject события;
|
||||||
- детальная JSON Schema — в `bitrix-local-app/openapi.yaml` и `api-backend/openapi.yaml`.
|
- детальная JSON Schema — в `bitrix-local-app/openapi.yaml` и `api-backend/openapi.yaml`.
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ Reverse proxy и единственная публичная точка вход
|
|||||||
- передает 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);
|
- если входящий запрос **без** `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`;
|
||||||
|
- для `POST /api/v1/dialogs/*/messages` `proxy_read_timeout` должен быть не меньше `MESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s`, чтобы nginx не обрывал sync-wait при async file scan;
|
||||||
- применяет edge rate limits для auth, API и download endpoints;
|
- применяет edge rate limits для auth, API и download endpoints;
|
||||||
- ограничивает частоту соединений и размер тела запроса;
|
- ограничивает частоту соединений и размер тела запроса;
|
||||||
- разрешает только TLS 1.2/1.3 и запрещает слабые шифры;
|
- разрешает только TLS 1.2/1.3 и запрещает слабые шифры;
|
||||||
@@ -190,7 +191,7 @@ Python worker/service **двусторонней** синхронизации Ap
|
|||||||
- не блокирует пользовательский API при ошибках Битрикс24;
|
- не блокирует пользовательский API при ошибках Битрикс24;
|
||||||
- не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`;
|
- не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`;
|
||||||
- **не участвует** в hot path чата Open Lines;
|
- **не участвует** в hot path чата Open Lines;
|
||||||
- включается/отключается флагом **`BITRIX_SYNC_ENABLED`** в `.env` (default `true`): при `false` сервис не стартует или работает в no-op (синхронизация с Bitrix24 CRM не выполняется).
|
- включается/отключается флагом **`BITRIX_SYNC_ENABLED`** в `.env` (default `true`): при `false` сервис стартует в no-op/degraded режиме, но не обрабатывает `sync_queue` и не выполняет синхронизацию с Bitrix24 CRM.
|
||||||
|
|
||||||
### bitrix-local-app
|
### bitrix-local-app
|
||||||
|
|
||||||
@@ -283,7 +284,7 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
|||||||
|
|
||||||
## Переменные окружения
|
## Переменные окружения
|
||||||
|
|
||||||
Корневой `backend/.env` читается всеми сервисами compose через `${VAR}` в сервисных `docker-compose.yml`. Канонический `.env.example`, service tokens, `app_settings` — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md).
|
Корневой `backend/.env` читается всеми сервисами compose через `${VAR}` в сервисных `docker-compose.yml`. Канонический `.env.example` и `app_settings` — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md); контракты service tokens — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
|
||||||
|
|
||||||
## HTTPS и TLS
|
## HTTPS и TLS
|
||||||
|
|
||||||
@@ -293,7 +294,7 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
|||||||
|
|
||||||
| Host | Порт 80 | Порт 443 | Примечание |
|
| Host | Порт 80 | Порт 443 | Примечание |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Веб-домен (frontend) | только `301`/`308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru` / `app.example.ru` |
|
| Веб-домен (frontend) | только `301`/`308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru`; staging/dev может использовать отдельный host |
|
||||||
| API-домен (если выделен) | **не слушает** | только HTTPS | Post-MVP: `api.example.ru` |
|
| API-домен (если выделен) | **не слушает** | только HTTPS | Post-MVP: `api.example.ru` |
|
||||||
| Bitrix callbacks (`/bitrix/*`, `/bitrix/sync/*`) | не обслуживает API; только redirect на том же host | HTTPS | webhook и install URL |
|
| Bitrix callbacks (`/bitrix/*`, `/bitrix/sync/*`) | не обслуживает API; только redirect на том же host | HTTPS | webhook и install URL |
|
||||||
|
|
||||||
@@ -325,7 +326,7 @@ Identity provider. **Обязателен** в compose-контуре с пер
|
|||||||
|
|
||||||
Рекомендуемая схема:
|
Рекомендуемая схема:
|
||||||
|
|
||||||
- **веб-домен** (MVP: `tohin.ru` или `app.example.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация;
|
- **веб-домен** (MVP: `tohin.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация;
|
||||||
- **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`;
|
- **выделенный 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`;
|
- для `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`;
|
||||||
@@ -400,13 +401,15 @@ 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`: `/health/live` проверяет процесс; `/health/ready` проверяет PostgreSQL `han_app`, Redis `/0` и `/1`, доступность JWKS/discovery Keycloak, S3 permissions для presign/promote и readiness `message-safety`;
|
||||||
- `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`: HTTP 200 от `/health/live` и `/health/ready` (ready — PostgreSQL + доступ к `sync_queue`);
|
- `bitrix-sync`: `/health/live` проверяет процесс; `/health/ready` проверяет PostgreSQL, доступ к `sync_queue`, worker state и CRM webhook config; при `BITRIX_SYNC_ENABLED=false` ready возвращает degraded/not-ready с причиной `sync_disabled`;
|
||||||
- `bitrix-local-app`: HTTP 200 от `/health/live`, readiness показывает наличие OAuth-токенов после установки приложения;
|
- `bitrix-local-app`: `/health/live` проверяет процесс; `/health/ready` показывает PostgreSQL, OAuth-токены после установки приложения, connector activation и возможность forward в API при включённом `BITRIX_API_FORWARD_URL`;
|
||||||
- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;
|
- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;
|
||||||
- `redis`: `redis-cli ping`;
|
- `redis`: `redis-cli ping`;
|
||||||
|
|
||||||
|
Наружу через `nginx` публикуются только health endpoint, которые нужны Bitrix24 install/callback validation или внешнему мониторингу. Internal services (`message-safety`, internal `bitrix-sync`, Redis, otel) проверяются только из Docker/VPC-сети.
|
||||||
|
|
||||||
## Порядок запуска
|
## Порядок запуска
|
||||||
|
|
||||||
1. `redis` (managed PostgreSQL должна быть доступна до старта зависимых сервисов).
|
1. `redis` (managed PostgreSQL должна быть доступна до старта зависимых сервисов).
|
||||||
@@ -449,3 +452,10 @@ Docker Compose на одной VM — production-контур первого э
|
|||||||
- backup и restore;
|
- backup и restore;
|
||||||
- централизованный мониторинг;
|
- централизованный мониторинг;
|
||||||
- горизонтальное масштабирование API и worker.
|
- горизонтальное масштабирование API и worker.
|
||||||
|
|
||||||
|
### Backup, restore и cleanup
|
||||||
|
|
||||||
|
- Managed PostgreSQL должен иметь ежедневные backups и PITR; целевые RPO/RTO для MVP фиксируются в ops runbook до production-запуска.
|
||||||
|
- S3-data (`attachments`, `documents`) хранит production-файлы; удаление выполняется только через lifecycle, retention или явный audit-backed процесс.
|
||||||
|
- S3-quarantine очищается периодическим cleanup job: удаляются просроченные объекты без активного `MessageAttachment`/`safety_tasks` или объекты с завершённым deny/failed lifecycle.
|
||||||
|
- Redis не является единственным хранилищем бизнес-событий; потеря Redis не должна терять сообщения, sync tasks или audit.
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Managed PostgreSQL **поднимается до** развёртывания п
|
|||||||
| Группа | Ключи |
|
| Группа | Ключи |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Auth | `auth.phone.enabled`, `auth.password.enabled` |
|
| Auth | `auth.phone.enabled`, `auth.password.enabled` |
|
||||||
| OTP (продукт; потребитель — Keycloak SPI, не api-backend) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` |
|
| OTP (продукт; потребитель — Keycloak SPI через settings bridge 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.*` |
|
||||||
@@ -120,6 +120,7 @@ chat.attachments.max_size_mb=5
|
|||||||
chat.attachments.storage=selectel_s3
|
chat.attachments.storage=selectel_s3
|
||||||
chat.attachments.upload_mode=presigned_put
|
chat.attachments.upload_mode=presigned_put
|
||||||
chat.attachments.safety_scan_required=true
|
chat.attachments.safety_scan_required=true
|
||||||
|
chat.attachments.presigned_upload_ttl_seconds=600
|
||||||
|
|
||||||
rate_limit.message_send.per_user=30/minute
|
rate_limit.message_send.per_user=30/minute
|
||||||
rate_limit.message_send.per_dialog=20/minute
|
rate_limit.message_send.per_dialog=20/minute
|
||||||
@@ -129,7 +130,7 @@ rate_limit.login.per_ip=10/minute
|
|||||||
|
|
||||||
ux.session.idle_timeout_minutes=30
|
ux.session.idle_timeout_minutes=30
|
||||||
|
|
||||||
security.cors.allowed_origins=https://tohin.ru,https://app.example.ru
|
security.cors.allowed_origins=https://tohin.ru
|
||||||
security.public_cache.max_age_seconds=3600
|
security.public_cache.max_age_seconds=3600
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -165,9 +166,9 @@ KC_DB_URL_PROPERTIES=currentSchema=keycloak
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Публичные URL (HTTPS)
|
# Публичные URL (HTTPS)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
PUBLIC_WEB_URL=https://app.example.ru
|
PUBLIC_WEB_URL=https://tohin.ru
|
||||||
PUBLIC_API_URL=https://app.example.ru/api
|
PUBLIC_API_URL=https://tohin.ru/api
|
||||||
PUBLIC_AUTH_URL=https://app.example.ru/auth
|
PUBLIC_AUTH_URL=https://tohin.ru/auth
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# nginx (edge, TLS, rate limits)
|
# nginx (edge, TLS, rate limits)
|
||||||
@@ -188,7 +189,7 @@ NGINX_RATE_LIMIT_POLLING=60r/m
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Keycloak (infra; OTP-заглушка — dev/MVP)
|
# Keycloak (infra; OTP-заглушка — dev/MVP)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
KEYCLOAK_PUBLIC_URL=https://app.example.ru/auth
|
KEYCLOAK_PUBLIC_URL=https://tohin.ru/auth
|
||||||
KEYCLOAK_INTERNAL_URL=http://keycloak:8080
|
KEYCLOAK_INTERNAL_URL=http://keycloak:8080
|
||||||
KEYCLOAK_REALM=han-chat
|
KEYCLOAK_REALM=han-chat
|
||||||
KEYCLOAK_AUDIENCE=han-chat-api
|
KEYCLOAK_AUDIENCE=han-chat-api
|
||||||
@@ -214,6 +215,7 @@ BITRIX_API_INBOX_TOKEN=change-me
|
|||||||
BITRIX_INTERNAL_API_TOKEN=change-me
|
BITRIX_INTERNAL_API_TOKEN=change-me
|
||||||
BITRIX_API_FORWARD_TOKEN=change-me
|
BITRIX_API_FORWARD_TOKEN=change-me
|
||||||
BITRIX_SYNC_SERVICE_TOKEN=change-me
|
BITRIX_SYNC_SERVICE_TOKEN=change-me
|
||||||
|
KEYCLOAK_SETTINGS_BRIDGE_TOKEN=change-me
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# api-backend (интеграции + resilience I2)
|
# api-backend (интеграции + resilience I2)
|
||||||
@@ -258,6 +260,7 @@ BITRIX_APPLICATION_TOKEN=change-me
|
|||||||
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
|
||||||
|
MESSAGE_SAFETY_FILE_SCAN_TIMEOUT_SEC=60
|
||||||
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
|
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -301,7 +304,22 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
|||||||
| Значение | Поведение |
|
| Значение | Поведение |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `true` (default) | `bitrix-sync` обрабатывает `sync_queue` и принимает CRM webhook |
|
| `true` (default) | `bitrix-sync` обрабатывает `sync_queue` и принимает CRM webhook |
|
||||||
| `false` | синхронизация с Bitrix24 CRM не выполняется (сервис не стартует или no-op); чат Open Lines через `bitrix-local-app` **не** затрагивается |
|
| `false` | синхронизация с Bitrix24 CRM не выполняется; сервис стартует в no-op/degraded режиме; чат Open Lines через `bitrix-local-app` **не** затрагивается |
|
||||||
|
|
||||||
|
В MVP выбран режим **no-op service**: контейнер `bitrix-sync` стартует, `/health/live` отвечает успешно, `/health/ready` возвращает degraded/not-ready с явной причиной `sync_disabled`, worker не обрабатывает `sync_queue`, webhook CRM возвращает безопасный `503` или `202 ignored` по контракту модуля. Это сохраняет единый compose-контур и не влияет на чат Open Lines.
|
||||||
|
|
||||||
|
## Keycloak settings bridge для OTP
|
||||||
|
|
||||||
|
Product limits OTP (`otp.phone.*`) хранятся в `app_settings`, но Keycloak не получает прямой доступ к схеме `han_app`.
|
||||||
|
|
||||||
|
MVP-механизм:
|
||||||
|
|
||||||
|
1. `api-backend` читает публичные/служебные настройки из `app_settings` и кэширует их.
|
||||||
|
2. Для Keycloak SPI доступен internal endpoint `GET /internal/settings/v1/otp` в Docker/VPC-сети, защищённый service token.
|
||||||
|
3. Keycloak SPI читает `otp.phone.max_send_attempts_per_24h` и `otp.phone.min_seconds_between_attempts` через этот endpoint с локальным cache TTL.
|
||||||
|
4. При недоступности settings bridge SPI использует последнее валидное cache-значение; если cache пустой — fail-closed и не выдаёт OTP.
|
||||||
|
|
||||||
|
Счётчики попыток OTP остаются в зоне Keycloak/SPI, не в `api-backend`.
|
||||||
|
|
||||||
## Разрешённые типы файлов чата (MVP)
|
## Разрешённые типы файлов чата (MVP)
|
||||||
|
|
||||||
|
|||||||
@@ -72,13 +72,17 @@ Raw OTP запрещено хранить в открытом виде: это
|
|||||||
|
|
||||||
- идемпотентность обработки задач очереди;
|
- идемпотентность обработки задач очереди;
|
||||||
- поведение при повторной доставке webhook;
|
- поведение при повторной доставке webhook;
|
||||||
- таймауты и retry/backoff.## Definition of Done
|
- таймауты и retry/backoff.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
Модуль считается готовым, если:
|
Модуль считается готовым, если:
|
||||||
|
|
||||||
- реализованы сценарии из задачи;
|
- реализованы сценарии из задачи;
|
||||||
- обновлен `{service}/openapi.yaml`, если менялся HTTP API;
|
- обновлен `{service}/openapi.yaml`, если менялся HTTP API;
|
||||||
|
- обновлены каталог ошибок в `arch-02` и contract tests, если менялась публичная или internal HTTP-семантика;
|
||||||
- созданы миграции, если менялась БД;
|
- созданы миграции, если менялась БД;
|
||||||
|
- обновлены seed `app_settings` и `.env.example`, если добавлялись настройки, service tokens, лимиты или feature flags;
|
||||||
- добавлены тесты;
|
- добавлены тесты;
|
||||||
- сервис запускается в Docker Compose;
|
- сервис запускается в Docker Compose;
|
||||||
- все изменяемые параметры вынесены из кода;
|
- все изменяемые параметры вынесены из кода;
|
||||||
|
|||||||
Reference in New Issue
Block a user