26 KiB
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и по возможности W3Ctraceparent. - 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и вложение →400mixed_content_not_allowed(доmessage-safety); - пустое сообщение →
403empty_message; - более одного вложения →
400too_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).
Подключение:
- Клиент открывает WS с валидным access token.
- Сервер отправляет
{ "type": "connected", "server_time": "ISO8601" }. - Клиент отправляет подписку:
{ "type": "subscribe", "dialog_ids": ["uuid"] }
- Сервер отвечает
{ "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_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 чата.
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-сеть.