Проект разделен на два репозитория

This commit is contained in:
mi
2026-08-14 15:42:45 +03:00
parent e06a77ee1d
commit bbef7a30c9
521 changed files with 2597 additions and 2302 deletions
+45 -6
View File
@@ -9,8 +9,8 @@
## Правила связности
- Любой новый 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/*`.
- Публичные пользовательские API находятся под `/api/v1`; public listeners nginx `80/443` не публикуют `/internal/*`. Канонический production ingress Safety — отдельный private listener nginx ВМ2 `:8443` с internal CA, source allow-list и service token; это не public route и не Docker HTTP fallback.
- Internal HTTP API между backend-сервисами используют единую маску: **`/internal/{service_mnemonic}/v1/{resource}`**, где `{service_mnemonic}` — короткое имя владельца endpoint (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Мнемоники internal API»). Утверждённое исключение — target Message Safety `/internal/safety/v2/*`; legacy `/internal/safety/v1/*` остаётся только stub до cutover и на private `:8443` не публикуется. 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.
@@ -18,7 +18,7 @@
## Service tokens (internal API)
Все internal endpoint (`/internal/*`) доступны **только** из Docker/VPC-сети и требуют service token. Endpoint не публикуются через `nginx` (исключение — ops внутри VPC).
Все internal endpoint (`/internal/*`) доступны **только** из Docker/VPC-сети и требуют service token. Public listeners nginx их не публикуют. Private nginx ВМ2 `:8443` является утверждённым ingress для Safety hot path и allow-listed ops endpoint внутри VPC.
| Переменная | Кто проверяет | Кто передаёт | Endpoint | Заголовок |
|---|---|---|---|---|
@@ -114,6 +114,7 @@
| `403` | `forbidden` | Доступ запрещён и ресурс не скрывается | нет |
| `404` | `not_found` | Ресурс не существует или принадлежит другому пользователю | нет |
| `409` | `idempotency_key_reused` | Тот же `Idempotency-Key` с другим fingerprint | нет |
| `409` | `resource_state_conflict` | JWT валиден, но локальный `UserIdentity` ещё не создан через bootstrap, либо ресурс находится в несовместимом lifecycle state | после bootstrap либо изменения state |
| `409` | `notification_conflict` | `(source, external_id)` уже занят Create с другим fingerprint | нет |
| `409` | `notification_closed` | Действие по уже закрытому уведомлению | нет |
| `422` | `message_blocked` | Message Safety вернул final deny | нет |
@@ -126,6 +127,8 @@
Правило доступа к пользовательским ресурсам: для `dialog_id`, `message_id`, `attachment_id`, `document_id`, принадлежащих другому `user_id`, api-backend по умолчанию возвращает `404 not_found`, чтобы не раскрывать существование ресурса. `403 forbidden` используется только для операций, где сам факт ресурса уже известен пользователю или оператору.
Для любого protected endpoint, кроме самого `POST /api/v1/auth/bootstrap`, валидный JWT при отсутствии локального `UserIdentity` возвращает `409 resource_state_conflict` с generic сообщением `bootstrap required`. Frontend после такого ответа выполняет bootstrap один раз и повторяет исходную операцию с тем же idempotency key, если она идемпотентна.
### `POST /api/v1/auth/bootstrap` (после OTP)
Вызывается **один раз** после успешного OTP и получения JWT. Создаёт локального пользователя и **сразу** сохраняет согласия из тела (атомарно в одной транзакции). **Не** создаёт UX-сессию — для этого используется `POST /api/v1/analytics/session-start`.
@@ -182,7 +185,7 @@
}
```
api-backend сохраняет согласия с привязкой к **`user_id`** из JWT. Пользователь должен уже существовать (`bootstrap` выполнен), иначе **`404`** / **`409`** по контракту модуля. Обязательные согласия без `accepted: true`**`403`** `consents_required`.
api-backend сохраняет согласия с привязкой к **`user_id`** из JWT. Пользователь должен уже существовать (`bootstrap` выполнен), иначе `409 resource_state_conflict`. Обязательные согласия без `accepted: true`**`403`** `consents_required`.
### `POST /api/v1/analytics/session-start` (событие `session_start`)
@@ -499,12 +502,46 @@ Frontend не обращается напрямую к Keycloak DB и не хр
|---|---|---|---|---|
| `POST /internal/safety/v2/messages/check` | `message-safety` | `api-backend` | Проверка текста, ссылок и файлов | private HTTPS + internal CA + `X-Service-Token` |
| `GET /internal/safety/v2/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос до финального вердикта **внутри** того же public `POST .../messages` | private HTTPS + internal CA + `X-Service-Token` |
| `GET /internal/safety/status` | nginx ВМ2 → `message-safety /health/ready` | `api-backend`, ops | Короткоживущий capability snapshot; не correctness gate | private HTTPS + internal CA + source allow-list |
| Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key |
HTTP-семантика target v2 от `message-safety`: `200 allow`, `403 deny`, `202 Accepted/pending`. Текущие `/v1/*` и `203` относятся только к legacy stub и не являются production-контрактом.
Канонический wire DTO `POST .../check`:
```json
{"message_id":"uuid","content_kind":"text","text":"Текст сообщения","attachment":null}
```
```json
{
"message_id":"uuid",
"content_kind":"file",
"text":"",
"attachment":{
"attachment_id":"uuid",
"quarantine_object_key":"quarantine/users/{user_id}/dialogs/{dialog_id}/{attachment_id}",
"quarantine_version_id":"opaque-version-id",
"quarantine_etag":"\"etag\"",
"mime_type":"application/pdf",
"size_bytes":12345,
"checksum":"sha256:<64-lowercase-hex>"
}
}
```
DTO является strict discriminated union, unknown fields запрещены. Caller маппит App DB `checksum_sha256` в `attachment.checksum` с обязательным prefix `sha256:`; `quarantine_version_id` и `quarantine_etag` передаются без переименования.
Normative details v2: каждый verdict/pending содержит `processing_mode=standard|mock` и `config_version`; `202` обязательно содержит `Location`, `Retry-After`, `task_id`, `expires_at` и существует только в standard mode; terminal `503 task_failed``terminal=true,retryable=false`; transient `503 dependency_unavailable``terminal=false,retryable=true`; `409 safety_request_conflict` — non-retryable caller invariant. Все domain deny имеют `reason_code=message_blocked`.
`Location` должен быть origin-relative path `/internal/safety/v2/messages/tasks/{task_id}`. Caller и recovery job резолвят его относительно origin `MESSAGE_SAFETY_URL`; absolute URL, другой host или path вне этого prefix отклоняются как нарушение контракта без HTTP-запроса.
CA-пара caller: `MESSAGE_SAFETY_CA_HOST_PATH` задаёт root-owned host bind, `MESSAGE_SAFETY_CA_FILE` — путь к нему внутри контейнера `api-backend`. Для remote production URL обязательны оба уровня доставки; TLS verification отключать запрещено.
Caller budget: POST timeout `MESSAGE_SAFETY_POST_TIMEOUT_SEC=5`, одна попытка GET task — не более `2` секунд, client sync-poll budget `MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300`, durable recovery budget `HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200`. Числа являются initial defaults из arch-04; изменение выполняется синхронно в arch-04 и профильных модулях.
Internal error subset не смешивается с public JWT errors: `401 service_unauthorized`, `400 validation_error`, `404 task_not_found`, `409 safety_request_conflict`, `429 rate_limit_exceeded`, `500 internal_error`, `503 dependency_unavailable|task_failed`. Поля и retry-семантика определены в `module-05` §8.3.
Emergency MOCK включается только root-owned helper/restart на ВМ2. В MOCK нет content/link/file checks и `202`: `TEXT_FREE`/`FILE_FREE=true` → sync `200`, false → canonical sync `403`. Auth/DTO/idempotency/audit/rate limits сохраняются. Public API не раскрывает `processing_mode`.
Поведение `api-backend`:
@@ -514,6 +551,8 @@ Emergency MOCK включается только root-owned helper/restart на
3. При `202` сохраняет `task_id`, `Location`, deadline и **не ставит задачу в свою очередь анализа**; синхронно поллит `Location`, соблюдая `Retry-After`, до `200`/`403`, terminal failed `503` или timeout.
4. Решение «проверка быстрая или долгая» — только у `message-safety`. Ожидание poll держит **одно** клиентское HTTP-соединение; это не блокирует обработку других запросов (параллельные workers/async).
`api-backend` не вызывает `/internal/sync/v1/*`. Этот ops-only namespace может находиться на том же private listener `:8443`, но защищается отдельным `BITRIX_SYNC_SERVICE_TOKEN` и path allow-list.
Checkpoint: на время poll — запись в **`safety_tasks`** (`han_app`) для recovery при crash/timeout (I1), не очередь анализа.
Recovery contract для `han_app.safety_tasks`:
@@ -622,7 +661,7 @@ Circuit breaker + timeout budget (I2): при открытом circuit на `mes
`contact.rebind` не является задачей `han_app.sync_queue`: это audited административный workflow, создаваемый только через `bitrix_sync.request_bitrix_contact_rebind`.
`bitrix-sync` **не создаёт** `UserIdentity` / `ClientProfile` в auth-flow; вход worker — задачи из `sync_queue`, созданные триггерами.
`entity_id` contact-задачи всегда равен `UserIdentity.id`; payload не содержит PII snapshot. Полный DDL/state-machine contract — [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md), §§69.
`entity_id` contact-задачи всегда равен `UserIdentity.id`; payload не содержит PII snapshot. Полный DDL/state-machine contract — [`module-07-bitrix-sync.md`](../VM2_services/documentation/module-07-bitrix-sync.md), §§69.
### Internal HTTP `bitrix-sync` (ops, не hot path)
@@ -707,7 +746,7 @@ Raw OTP и полный номер телефона в audit **не** пишут
| `request_id` | из `X-Request-ID` |
| `ip`, `user_agent` | из proxy headers |
Presigned URL и содержимое файла в audit **не** пишутся. Структура таблицы — модуль `database`.
Presigned URL и содержимое файла в audit **не** пишутся. Структура таблицы принадлежит `module-01-api-backend` и migration owner схемы `han_app`.
## Health-контракты