Files
han-app/architectory/arch-02-api-contracts.md
T
2026-07-09 11:03:44 +03:00

26 KiB
Raw Blame History

arch-02. API-контракты и связность взаимодействий

Термины — в arch-00-glossary.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, «Мнемоники 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).
  • Все 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 Сохранение согласий перед 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/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 Инициализация загрузки в S3-quarantine JWT
POST /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/complete api-backend Expo frontend Завершение загрузки и фиксация checksum/metadata JWT
WS /api/v1/realtime api-backend Expo frontend Realtime-события чата, статусы доставки, unread JWT

Единый формат ошибки:

{
  "error": {
    "code": "profile_not_found",
    "message": "Profile was not found",
    "request_id": "01J00000000000000000000000",
    "details": {}
  }
}

POST /api/v1/consents (тело запроса)

{
  "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" },
    "marketing": { "accepted": false, "version": "2026-06-10" }
  },
  "device": {
    "platform": "ios",
    "app_version": "1.0.0",
    "device_id": "..."
  }
}

После OTP api-backend связывает запись с UserIdentity по guest_session_id (в рамках POST /api/v1/auth/bootstrap). TTL guest-записи — 24 ч.

POST /api/v1/analytics/session-start (событие session_start)

Вызывается frontend только при начале новой UX-сессии (см. arch-01, «Аналитическая UX-сессия»). Не привязан к OTP и JWT.

Заголовки: X-Request-ID (опционально).

Тело:

{
  "start_reason": "first_launch",
  "guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
  "device": {
    "platform": "web",
    "app_version": "1.0.0",
    "device_id": "..."
  }
}
  • start_reason — обязательно: first_launch | cold_start | idle_timeout;
  • guest_session_id — опционально (если уже создан в гостевом режиме).

Ответ 201:

{
  "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).

POST /api/v1/auth/bootstrap (после OTP)

Вызывается один раз после успешного OTP и получения JWT. Не создаёт UX-сессию.

Заголовки: Authorization: Bearer <access_token>, X-Ux-Session-Id (рекомендуется).

Тело:

{
  "guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
  "ux_session_id": "660e8400-e29b-41d4-a716-446655440001"
}

Ответ 200: { "user_id": "uuid", "profile_ready": true }.

Ошибки: 401 (JWT), 403 (согласия не приняты / guest session истёк).

Создание диалога

  • POST /api/v1/dialogs — idempotency key в заголовке; ответ { "dialog_id": "uuid", "status": "open" }.
  • Обязателен перед первым POST .../messages (включая популярный вопрос после auth).
  • dialog_id = external_chat_id (см. arch-00-glossary.md).

Формат исходящего сообщения клиента (MVP)

Имена content_kind, полей — 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);
  • пустое сообщение → 403 empty_message;
  • более одного вложения → 400 too_many_attachments;
  • файловое сообщение в Bitrix24: message.files (signed URL), message.text пустой.

Post-MVP: допускается «текст + файлы» отдельной версией API.

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. Клиент отправляет подписку:
{ "type": "subscribe", "dialog_ids": ["uuid"] }
  1. Сервер отвечает { "type": "subscribed", "dialog_ids": ["uuid"] }.

События сервер → клиент:

type Назначение Ключевые поля
message.new Новое сообщение в диалоге dialog_id, message (DTO как в REST)
message.status Смена статуса доставки/safety dialog_id, message_id, 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=<cursor>.

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

Контракт Владелец Потребитель Назначение
OIDC Authorization Code Flow with PKCE Keycloak Expo frontend OTP-only login, token issue, refresh
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 и ключей

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). api-backend OTP не проверяет, только JWT.

Жизненный цикл 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 Синхронная проверка текста, ссылок и файлов по 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
Read S3-quarantine Selectel S3 message-safety Чтение файла worker-ом при cache miss read-only key

HTTP-семантика: 200 allow, 403 deny, 203 pending. api-backend не выбирает sync/async режим, а только интерпретирует ответ.

Маппинг в App DB (Message.safety_status — см. arch-00-glossary.md):

HTTP / message-safety Message.safety_status Финальный?
200 / allow allowed да
403 / deny blocked да
203 / pending pending нет

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_idbitrix_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 чата.

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, OTP counters, coordination/realtime state

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
guest_session_id UUID или null
user_id null (до auth bootstrap)
request_id из X-Request-ID
ip, user_agent из proxy headers

Raw OTP и полный номер телефона в audit не пишутся.

Audit: выдача download URL

При GET /api/v1/documents/{document_id}/download-url и аналогичных endpoint вложений чата 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-сеть.