Grok version
This commit is contained in:
@@ -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/*`.
|
||||
- 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`.
|
||||
- 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-сетью.
|
||||
|
||||
## 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/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/analytics/session-start` | `api-backend` | Expo frontend | Событие `session_start`, новая `UxSession` | public + rate limit |
|
||||
| `POST /api/v1/auth/bootstrap` | `api-backend` | Expo frontend | После OTP: `find-or-create` пользователя, связь согласий, привязка `user_id` к `UxSession` | JWT |
|
||||
| `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` (только для авторизованного пользователя) | JWT + rate limit |
|
||||
| `POST /api/v1/auth/bootstrap` | `api-backend` | Expo frontend | После OTP: `find-or-create` пользователя + сохранение согласий из тела запроса | JWT |
|
||||
| `POST /api/v1/dialogs` | `api-backend` | Expo frontend | Создание диалога перед первым сообщением (в т.ч. после популярного вопроса) | JWT + idempotency |
|
||||
| `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/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/{dialog_id}` | `api-backend` | Expo frontend | Карточка диалога | 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}/attachments/init` | `api-backend` | Expo frontend | Инициализация загрузки в 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/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 | Подтверждение загрузки, проверка объекта в 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 |
|
||||
|
||||
Единый формат ошибки:
|
||||
@@ -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
|
||||
{
|
||||
"guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"consents": {
|
||||
"personal_data": { "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`)
|
||||
|
||||
Вызывается frontend **только** при начале **новой** UX-сессии (см. arch-01, «Аналитическая UX-сессия»). **Не** привязан к OTP и JWT.
|
||||
Вызывается frontend **только** при начале **новой** UX-сессии у **авторизованного** пользователя (есть валидный JWT и обычно уже выполнен `bootstrap`). **Не** вызывается в гостевом режиме.
|
||||
|
||||
**Заголовки:** `X-Request-ID` (опционально).
|
||||
**Заголовки:** `Authorization: Bearer <access_token>`, `X-Request-ID` (опционально).
|
||||
|
||||
**Тело:**
|
||||
|
||||
```json
|
||||
{
|
||||
"start_reason": "first_launch",
|
||||
"guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"device": {
|
||||
"platform": "web",
|
||||
"app_version": "1.0.0",
|
||||
@@ -118,8 +156,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
- `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`;
|
||||
- `guest_session_id` — опционально (если уже создан в гостевом режиме).
|
||||
- `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`.
|
||||
|
||||
**Ответ `201`:**
|
||||
|
||||
@@ -134,31 +171,26 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
|
||||
|
||||
**Повторный вызов в рамках той же UX-сессии не требуется** (возврат из фона в пределах idle timeout).
|
||||
|
||||
### `POST /api/v1/auth/bootstrap` (после OTP)
|
||||
|
||||
Вызывается **один раз** после успешного 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 истёк).
|
||||
**Ошибки:** `401` без/с невалидным JWT.
|
||||
|
||||
### Создание диалога
|
||||
|
||||
- `POST /api/v1/dialogs` — idempotency key в заголовке; ответ `{ "dialog_id": "uuid", "status": "open" }`.
|
||||
- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth).
|
||||
- `POST /api/v1/dialogs` — заголовок **`Idempotency-Key`** (обязателен); ответ `{ "dialog_id": "uuid", "status": "open" | "waiting_for_company" | "waiting_for_client" }`.
|
||||
- **Один активный диалог** на пользователя: если уже есть диалог со статусом не `closed`, endpoint возвращает его (idempotent), новый не создаёт.
|
||||
- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth), если у клиента ещё нет `dialog_id`.
|
||||
- `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)
|
||||
|
||||
Имена `content_kind`, полей — [`arch-00-glossary.md`](arch-00-glossary.md). Правила:
|
||||
@@ -169,12 +201,28 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
|
||||
| `file` | `attachment_id` + `checksum`; `text` пустой | пустая строка | ровно 1 запись |
|
||||
|
||||
- непустой `text` **и** вложение → **`400`** `mixed_content_not_allowed` (до `message-safety`);
|
||||
- пустое сообщение → **`403`** `empty_message`;
|
||||
- пустое сообщение (нет `text` и нет `attachment_id`) → **`400`** `empty_message`;
|
||||
- более одного вложения → **`400`** `too_many_attachments`;
|
||||
- файловое сообщение в Bitrix24: `message.files` (signed URL), `message.text` пустой.
|
||||
|
||||
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`)
|
||||
|
||||
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` | Назначение | Ключевые поля |
|
||||
|---|---|---|
|
||||
| `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) |
|
||||
| `message.status` | Смена статуса доставки/safety | `dialog_id`, `message_id`, `status` |
|
||||
| `dialog.status` | Смена статуса диалога | `dialog_id`, `status` |
|
||||
| `message.status` | Смена `safety_status` / `delivery_status` | `dialog_id`, `message_id`, `safety_status`, `delivery_status` |
|
||||
| `dialog.status` | Смена `Dialog.status` | `dialog_id`, `status` |
|
||||
|
||||
**Reconnect:**
|
||||
|
||||
@@ -224,16 +272,30 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
|
||||
|
||||
## 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 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 отключена.
|
||||
|
||||
**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)
|
||||
|
||||
@@ -258,19 +320,31 @@ Frontend не обращается напрямую к Keycloak DB и не хр
|
||||
|
||||
| Контракт | Владелец | Потребитель | Назначение | Защита |
|
||||
|---|---|---|---|---|
|
||||
| `POST /internal/safety/v1/messages/check` | `message-safety` | `api-backend` | Синхронная проверка текста, ссылок и файлов по cache/rules | internal network + `X-Service-Token` |
|
||||
| `GET /internal/safety/v1/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос async-проверки файлов | 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` | Опрос до финального вердикта **внутри** того же `POST .../messages` | internal network + `X-Service-Token` |
|
||||
| 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` | Финальный? |
|
||||
|---|---|---|
|
||||
| `200` / `allow` | `allowed` | да |
|
||||
| `403` / `deny` | `blocked` | да |
|
||||
| `203` / `pending` | `pending` | нет |
|
||||
1. Синхронно вызывает `POST .../check`, получает один из трёх кодов.
|
||||
2. При `200` / `403` — сразу завершает сценарий и отвечает клиенту.
|
||||
3. При `203` — **не ставит задачу в свою очередь анализа**; регулярно и синхронно поллит `GET .../tasks/{task_id}` до `200`/`403` или timeout (`MESSAGE_SAFETY_TASK_POLL_MAX_SEC`), затем отвечает клиенту.
|
||||
4. Решение «проверка быстрая или долгая» — только у `message-safety`. Ожидание poll держит **одно** клиентское HTTP-соединение; это не блокирует обработку других запросов (параллельные workers/async).
|
||||
|
||||
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)
|
||||
|
||||
@@ -286,6 +360,38 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
|
||||
|
||||
`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
|
||||
|
||||
**без синхронного 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-attachments` | 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-контракты
|
||||
|
||||
@@ -367,8 +473,8 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
|
||||
| `event_type` | `session_start` |
|
||||
| `ux_session_id` | UUID новой UX-сессии |
|
||||
| `start_reason` | `first_launch` / `cold_start` / `idle_timeout` |
|
||||
| `guest_session_id` | UUID или null |
|
||||
| `user_id` | null (до auth bootstrap) |
|
||||
| `user_id` | из JWT |
|
||||
| `guest_session_id` | не используется (endpoint только с JWT) |
|
||||
| `request_id` | из `X-Request-ID` |
|
||||
| `ip`, `user_agent` | из proxy headers |
|
||||
|
||||
@@ -376,7 +482,7 @@ Raw OTP и полный номер телефона в audit **не** пишут
|
||||
|
||||
### 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 создаёт запись:
|
||||
|
||||
| Поле | Пример |
|
||||
|---|---|
|
||||
|
||||
Reference in New Issue
Block a user