# arch-02. API-контракты и связность взаимодействий > Термины — в [`arch-00-glossary.md`](arch-00-glossary.md). Общая схема — в [`arch-01-system-architecture.md`](arch-01-system-architecture.md). ## Назначение Этот документ — канонический реестр API-контрактов между frontend, backend-сервисами и внешними системами. Его цель — контролировать связность: если сервис описан как участник сценария, здесь должен быть указан контракт, направление вызова, владелец и потребитель. Когда появятся профильные спецификации модулей, они могут дублировать здесь зафиксированные контракты для удобства разработки. ## Правила связности - Любой новый endpoint, webhook, worker-contract или внешний вызов сначала добавляется в этот файл; при появлении профильного документа модуля-владельца — дублируется там для детализации реализации. - Публичные пользовательские API находятся под `/api/v1`; internal API не публикуются наружу через `nginx`. - 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`** во всех JWT-запросах к `api-backend`, когда UX-сессия активна (рекомендуется для аналитики и логов; **не** является auth). `session-start` и `consents` требуют JWT. - Все internal API защищаются service token и закрытой Docker/VPC-сетью. ## Service tokens (internal API) Все internal endpoint (`/internal/*`) доступны **только** из Docker/VPC-сети и требуют service token. Endpoint не публикуются через `nginx` (исключение — ops внутри VPC). | Переменная | Кто проверяет | Кто передаёт | Endpoint | Заголовок | |---|---|---|---|---| | `MESSAGE_SAFETY_SERVICE_TOKEN` | `message-safety` | `api-backend` | `POST/GET /internal/safety/v1/*` | `X-Service-Token` | | `BITRIX_INTERNAL_API_TOKEN` | `bitrix-local-app` | `api-backend` | `POST/GET /internal/openlines/v1/*` | `Authorization: Bearer` | | `BITRIX_LOCAL_APP_INTERNAL_TOKEN` | — | `api-backend` (исходящий) | то же | `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_SYNC_SERVICE_TOKEN` | `bitrix-sync` | ops / мониторинг | `GET /internal/sync/v1/*` | `Authorization: Bearer` или `X-Service-Token` | Пары значений (должны совпадать): - `BITRIX_LOCAL_APP_INTERNAL_TOKEN` (api-backend) = `BITRIX_INTERNAL_API_TOKEN` (bitrix-local-app) - `BITRIX_API_FORWARD_TOKEN` (bitrix-local-app) = `BITRIX_API_INBOX_TOKEN` (api-backend) Генерация: `openssl rand -hex 32`. Секреты не коммитить. **Не путать с webhook-токенами** (публичные callback от Bitrix24, не internal service API): | Переменная | Назначение | |---|---| | `BITRIX_APPLICATION_TOKEN` | проверка событий Bitrix24 → `bitrix-local-app` `/bitrix/handler` | | `BITRIX_SYNC_WEBHOOK_TOKEN` | проверка webhook Bitrix24 → `bitrix-sync` `/bitrix/sync/webhook/contact` | ## Frontend ↔ api-backend | Контракт | Владелец | Потребитель | Назначение | Auth | |---|---|---|---|---| | `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 | Повторное сохранение согласий (новые версии документов); привязка к `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/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 | Инициализация загрузки; ответ: `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 | Единый формат ошибки: ```json { "error": { "code": "profile_not_found", "message": "Profile was not found", "request_id": "01J00000000000000000000000", "details": {} } } ``` ### `POST /api/v1/auth/bootstrap` (после OTP) Вызывается **один раз** после успешного OTP и получения JWT. Создаёт локального пользователя и **сразу** сохраняет согласия из тела (атомарно в одной транзакции). **Не** создаёт UX-сессию — для этого `POST /analytics/session-start`. **Заголовки:** `Authorization: Bearer ` — **единственный** источник идентичности пользователя. **Как привязываются согласия (не через тело 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 { "consents": { "personal_data": { "accepted": true, "version": "2026-06-10" }, "user_agreement": { "accepted": true, "version": "2026-06-10" }, "marketing": { "accepted": false, "version": "2026-06-10" } }, "device": { "platform": "ios", "app_version": "1.0.0", "device_id": "..." } } ``` **Порядок после 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 `, `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-сессии у **авторизованного** пользователя (есть валидный JWT и обычно уже выполнен `bootstrap`). **Не** вызывается в гостевом режиме. **Заголовки:** `Authorization: Bearer `, `X-Request-ID` (опционально). **Тело:** ```json { "start_reason": "first_launch", "device": { "platform": "web", "app_version": "1.0.0", "device_id": "..." } } ``` - `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`. **Ответ `201`:** ```json { "ux_session_id": "660e8400-e29b-41d4-a716-446655440001", "started_at": "2026-07-08T12:00:00Z" } ``` Frontend сохраняет `ux_session_id` **в памяти** и передаёт **`X-Ux-Session-Id`** в последующих запросах. **Повторный вызов в рамках той же UX-сессии не требуется** (возврат из фона в пределах idle timeout). **Ошибки:** `401` без/с невалидным JWT. ### Создание диалога - `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). Правила: | `content_kind` | Тело `POST .../messages` | `Message.text` | `MessageAttachment` | |---|---|---|---| | `text` | непустой `text`; без вложения | текст | 0 записей | | `file` | `attachment_id` + `checksum`; `text` пустой | пустая строка | ровно 1 запись | - непустой `text` **и** вложение → **`400`** `mixed_content_not_allowed` (до `message-safety`); - пустое сообщение (нет `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`). **Подключение:** 1. Клиент открывает WS с валидным access token. 2. Сервер отправляет `{ "type": "connected", "server_time": "ISO8601" }`. 3. Клиент отправляет подписку: ```json { "type": "subscribe", "dialog_ids": ["uuid"] } ``` 4. Сервер отвечает `{ "type": "subscribed", "dialog_ids": ["uuid"] }`. **События сервер → клиент:** | `type` | Назначение | Ключевые поля | |---|---|---| | `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) | | `message.status` | Смена `safety_status` / `delivery_status` | `dialog_id`, `message_id`, `safety_status`, `delivery_status` | | `dialog.status` | Смена `Dialog.status` | `dialog_id`, `status` | **Reconnect:** - exponential backoff: 1s → 2s → 4s → … max 30s; - после reconnect — повтор `subscribe` с актуальным списком `dialog_ids`; - при недоступности WS > 30s — fallback на polling `GET .../messages?after=`. **Ping:** сервер может слать `{ "type": "ping" }` каждые 30s; клиент отвечает `{ "type": "pong" }`. ## OpenAPI | Сервис | Файл | Публикуется наружу | |---|---|---| | `api-backend` | `api-backend/openapi.yaml` | да (`/api/v1/*`, health) | | `message-safety` | `message-safety/openapi.yaml` | нет (internal) | | `bitrix-local-app` | `bitrix-local-app/openapi.yaml` | частично (`/bitrix/*`, health) | | `bitrix-sync` | `bitrix-sync/openapi.yaml` | нет (internal + webhook) | Правила: - breaking change публичного API → новый path-prefix (`/api/v2`) + запись в arch-02; - internal API версионируется тем же правилом (`/internal/{mnemonic}/v2/...`); - OpenAPI генерируется или поддерживается вручную — на усмотрение модуля, но файл обязателен в DoD (arch-05). ## Frontend ↔ Keycloak Keycloak **обязателен** в production-like контуре с первого запуска (OTP, tokens, JWKS). | Контракт | Владелец | Потребитель | Назначение | |---|---|---|---| | 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 | | 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 отключена. **`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) Детали — arch-01, «Обновление access token (frontend)». Кратко: | Механизм | Когда | Действие | |---|---|---| | **Scheduler** | за ~60 с до `exp` access token | Refresh Token Grant → новые tokens, перепланировать таймер | | **401 interceptor** | `api-backend` / WS отклонил access token | single-flight refresh → **один** retry запроса | | **Открытие приложения** | cold start / resume | silent refresh, если refresh token ещё действителен | Правила: - refresh выполняет **только frontend** (Keycloak token endpoint); `api-backend` на `401` **не** обновляет токен; - параллельные запросы при refresh — очередь + single-flight; - провал refresh → очистка tokens, гостевой режим, OTP при следующем защищённом действии; - успешный refresh **не** создаёт UX-сессию (`session_start`). **401 от `api-backend`:** единый формат ошибки (см. выше); типичный `code`: `unauthorized` / `token_expired` — frontend трактует как сигнал к refresh+retry (если refresh token ещё валиден). ## api-backend ↔ message-safety | Контракт | Владелец | Потребитель | Назначение | Защита | |---|---|---|---|---| | `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-семантика от `message-safety`: `200 allow`, `403 deny`, `203 pending`. Поведение `api-backend`: 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) Мнемоника сервиса: **`openlines`**. Endpoint Open Lines на стороне `bitrix-local-app` и приёмник событий на стороне `api-backend` используют один префикс `/internal/openlines/v1/`. | Контракт | Владелец | Потребитель | Назначение | Защита | |---|---|---|---|---| | `POST /internal/openlines/v1/messages` | `bitrix-local-app` | `api-backend` | Отправка сообщения клиента в Bitrix24 Open Lines | Bearer `BITRIX_INTERNAL_API_TOKEN` | | `GET /internal/openlines/v1/dialogs/{external_chat_id}` | `bitrix-local-app` | `api-backend` | Получение маппинга `dialog_id` ↔ `bitrix_chat_id` | Bearer token | | `GET /internal/openlines/v1/status` | `bitrix-local-app` | ops / `api-backend` | Статус OAuth и `imconnector.status` | Bearer token | | `POST /internal/openlines/v1/setup/retry` | `bitrix-local-app` | ops | Повтор register/activate/event.bind | Bearer token | | `POST /internal/openlines/v1/inbox` | `api-backend` | `bitrix-local-app` | Forward нормализованных событий оператора | Bearer `BITRIX_API_INBOX_TOKEN` | `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` **не создаёт** задачи синхронизации вручную. ### Очередь, триггеры и write-back (основной контракт MVP) | Контракт | Тип | Владелец | Потребитель | Назначение | |---|---|---|---|---| | `han_app.sync_queue` | PostgreSQL | триггеры `han_app` (миграции App DB) | `bitrix-sync` | Асинхронная очередь App DB → Bitrix24: триггер ставит задачу при изменении отслеживаемых полей | | `han.sync_suppress` (GUC) | PostgreSQL session | `bitrix-sync` | триггеры `han_app` | Подавление эхо-задач при записи данных от Bitrix24 в App DB | | `ClientProfile.bitrix_contact_id` | PostgreSQL | `bitrix-sync` | App DB | Маппинг профиля на CRM Contact после map/create | | `han_app.entity_external_mapping` | PostgreSQL | `bitrix-sync` | App DB | Универсальный маппинг App entity ↔ Bitrix entity (MVP: Contact) | | Обновление `sync_queue.status` | PostgreSQL | `bitrix-sync` | App DB | `processed` / `failed` / `dead_letter`, retry metadata | Типы задач MVP (`sync_queue.task_type`): - `contact.map_or_create` — матчинг/создание Contact, запись `bitrix_contact_id`, флаг регистрации в Bitrix24; - `contact.update` — push изменений профиля в Bitrix24. `bitrix-sync` **не создаёт** `UserIdentity` / `ClientProfile` в auth-flow; вход worker — задачи из `sync_queue`, созданные триггерами. ### Internal HTTP `bitrix-sync` (ops, не hot path) | Контракт | Владелец | Потребитель | Назначение | Защита | |---|---|---|---|---| | `GET /internal/sync/v1/status` | `bitrix-sync` | ops / мониторинг | Глубина очереди, dead letter, последний успешный run | internal network + `BITRIX_SYNC_SERVICE_TOKEN` | Повтор dead letter и ручной replay в MVP — через БД/ops-процедуры; отдельный HTTP replay-endpoint — post-MVP. ## bitrix-local-app ↔ Bitrix24 | Контракт | Направление | Назначение | |---|---|---| | `GET/POST /bitrix/install` | Bitrix24 → `bitrix-local-app` | Установка local app, OAuth lifecycle | | `GET/POST /bitrix/handler` | Bitrix24 → `bitrix-local-app` | `ONIMCONNECTOR*`, `ONAPPINSTALL`, `ONAPPUNINSTALL` | | `imconnector.register` | `bitrix-local-app` → Bitrix24 | Регистрация `han_mobile_app` | | `imconnector.activate` | `bitrix-local-app` → Bitrix24 | Привязка к линии 8 | | `event.bind` | `bitrix-local-app` → Bitrix24 | Подписка на события коннектора | | `imconnector.send.messages` | `bitrix-local-app` → Bitrix24 | Доставка сообщения клиента оператору | | `imconnector.send.status.delivery` | `bitrix-local-app` → Bitrix24 | Подтверждение доставки входящего события | ## bitrix-sync ↔ Bitrix24 CRM | Контракт | Направление | Назначение | |---|---|---| | `crm.contact.get/list/add/update` | `bitrix-sync` → Bitrix24 | Поиск, создание и обновление Contact | | `POST /bitrix/sync/webhook/contact` | Bitrix24 (робот) → `bitrix-sync` | Исходящий webhook при изменении полей Contact, зарегистрированного в приложении | | PostgreSQL schema `bitrix_sync` | `bitrix-sync` ↔ PostgreSQL | Worker state, field mapping, retry/dead letter audit | | PostgreSQL schema `han_app` | `bitrix-sync` ↔ PostgreSQL | Очередь `sync_queue`, маппинг ID, обновление профиля (Bitrix → App) | Очередь `han_app.sync_queue` и write-back — в разделе «api-backend ↔ bitrix-sync» выше. `bitrix-sync` использует `BITRIX_SYNC_APP_DATABASE_URL` для `han_app` + `bitrix_sync`, только `BITRIX_SYNC_CRM_*` для Bitrix24 CRM REST и не читает OAuth-токены `bitrix-local-app`. ## api-backend ↔ внешние хранилища | Контракт | Внешний сервис | Назначение | |---|---|---| | PostgreSQL schema `han_app` | Managed PostgreSQL | App DB: пользователи, профили, диалоги, сообщения, настройки, sync_queue, audit | | 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 API (`/0`), realtime/coordination (опц. `/1`); **не** OTP counters | ## Observability-контракты | Контракт | Владелец | Потребители | Назначение | |---|---|---|---| | OTLP gRPC/HTTP | `otel-collector` (`observability`) | backend-сервисы | Приём traces/logs/metrics | | JSON stdout logs | каждый сервис | platform logs / оператор | Техническая диагностика; **`ux_session_id`** из `X-Ux-Session-Id`, если передан | | Audit / analytics events в App DB | `api-backend` | аналитика, расследования | `session_start` и чувствительные действия без PII | ### Analytics: `session_start` При `POST /api/v1/analytics/session-start` api-backend создаёт запись: | Поле | Пример | |---|---| | `event_type` | `session_start` | | `ux_session_id` | UUID новой UX-сессии | | `start_reason` | `first_launch` / `cold_start` / `idle_timeout` | | `user_id` | из JWT | | `guest_session_id` | не используется (endpoint только с JWT) | | `request_id` | из `X-Request-ID` | | `ip`, `user_agent` | из proxy headers | Raw OTP и полный номер телефона в audit **не** пишутся. ### Audit: выдача download URL При `GET /api/v1/documents/{document_id}/download-url` и `GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url` api-backend создаёт запись: | Поле | Пример | |---|---| | `event_type` | `document.download_url_issued`, `attachment.download_url_issued` | | `user_id` | UUID пользователя | | `resource_type` | `document` / `attachment` | | `resource_id` | UUID ресурса | | `ux_session_id` | из `X-Ux-Session-Id` | | `request_id` | из `X-Request-ID` | | `ip`, `user_agent` | из proxy headers | Presigned URL и содержимое файла в audit **не** пишутся. Структура таблицы — модуль `database`. ## Health-контракты Все backend-сервисы имеют `GET /health/live` и `GET /health/ready`. Наружу публикуются только health endpoints, которые нужны `nginx`/Bitrix24; internal services проверяются через Docker/VPC-сеть.