Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)

This commit is contained in:
mi
2026-08-13 18:52:42 +03:00
parent 5100ba9fc3
commit 99605b1c77
144 changed files with 15295 additions and 1120 deletions
+1
View File
@@ -0,0 +1 @@
bitrix_local
+68 -41
View File
@@ -318,7 +318,7 @@ COMMIT
return stable user_id
```
Повторный bootstrap безопасен: уникальный ключ согласия не создаёт дубль; `last_login_at` обновляется. Триггеры на `UserIdentity`/`ClientProfile` создают `contact.map_or_create`/`contact.update` в `sync_queue`; application code задач не вставляет.
Повторный bootstrap безопасен: уникальный ключ согласия не создаёт дубль; `last_login_at` обновляется. Триггеры на `UserIdentity`/`ClientProfile` создают `contact.map_or_create`/`contact.update`/`contact.deactivate` в `sync_queue`; application code задач не вставляет. Полный trigger/dedup/lease contract задан в [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md), §6.
#### `POST /api/v1/consents`
@@ -430,7 +430,7 @@ Success `201` возвращает финальный `MessageResponse`:
}
```
На safety deny — `422 message_blocked`; blocked message допустимо сохранять для аудита, но его текст должен храниться по политике минимизации данных (см. решение M8). В той же транзакции backend создаёт отдельную локальную `company`-реплику с безопасным бизнес-текстом для сообщения или документа; эта реплика публикуется в realtime, но не отправляется в Open Lines. На dependency failure — `503/504`; если Message уже создан, его `delivery_status=failed`.
На safety deny — `422 message_blocked`; исходный blocked text не сохраняется: применяется M8. В той же транзакции backend создаёт отдельную локальную `company`-реплику с текстом из `text_resources.mnemonic=safety.chat.blocked`; internal `rule_id` клиенту не передаётся. Реплика публикуется как `message.new`, но не отправляется в Open Lines. На dependency failure — `503/504`; если Message уже создан, его `delivery_status=failed`.
### 6.7. Attachments
@@ -615,9 +615,9 @@ CHECK consent type: `personal_data | user_agreement | marketing`. Unique `(user_
### 9.5. `client_profiles`
`id`, `user_id` UNIQUE FK, `bitrix_contact_id NULL`, `full_name`, `citizenship`, `russian_phone`, `foreign_phone`, `email`, `source_updated_at`, common fields.
`id`, `user_id` UNIQUE FK, `full_name`, `citizenship`, `russian_phone`, `foreign_phone`, `email`, `source_updated_at`, common fields. CRM Contact ID в App DB не хранится; canonical mapping принадлежит schema `bitrix_sync`.
Индексы: unique active `user_id`; partial unique `bitrix_contact_id WHERE bitrix_contact_id IS NOT NULL AND record_status='A'`; `updated_at`. PII поля не включаются в логи и generic audit payload.
Индексы: unique active `user_id`; `updated_at`. PII поля не включаются в логи и generic audit payload.
### 9.6. `dialogs`
@@ -647,7 +647,7 @@ WHERE record_status='A';
### 9.7. `messages`
Поля: `id`, `dialog_id`, `sender_type`, `content_kind`, `text`, `safety_status`, `delivery_status`, `external_message_id NULL`, `client_idempotency_key NULL`, `occurred_at`, common fields.
Поля: `id`, `dialog_id`, `sender_type`, `content_kind`, `text`, `safety_status`, `safety_processing_mode` (`standard | mock`), `safety_config_version` bigint, `delivery_status`, `related_message_id NULL` (self-FK), `external_message_id NULL`, `client_idempotency_key NULL`, `occurred_at`, common fields. Safety mode/config version — internal audit fields и не входят в public DTO.
CHECK:
@@ -655,24 +655,25 @@ CHECK:
- content: `text | file`;
- safety: `pending | allowed | blocked` (`needs_review` зарезервирован, не создаётся);
- delivery: `accepted | processing | delivered | rejected | failed`;
- text message: `text <> ''`;
- text message: `text <> ''`, кроме blocked client message после M8 redaction (`text=''` допустим только при `sender_type=client AND safety_status=blocked`);
- file message: `text = ''`;
- company message: `safety_status='allowed'`;
- synthetic safety company-replica: `related_message_id` указывает на blocked client message, `content_kind=text`, `delivery_status=delivered`, `external_message_id=NULL`;
- rejected → blocked; delivered → allowed.
Индексы: `(dialog_id, created_at, id) WHERE record_status='A'`; `(delivery_status, updated_at)` для recovery; unique `(dialog_id, external_message_id)` where external id not null; unique `(dialog_id, client_idempotency_key)` where not null.
Индексы: `(dialog_id, created_at, id) WHERE record_status='A'`; `(delivery_status, updated_at)` для recovery; unique `(dialog_id, external_message_id)` where external id not null; unique `(dialog_id, client_idempotency_key)` where not null; unique `(related_message_id) WHERE sender_type='company' AND related_message_id IS NOT NULL`.
**Решение M3:** исходящее сообщение создаётся до safety со статусами `pending/accepted`, чтобы `safety_tasks` всегда имел FK и crash checkpoint. При начале poll delivery может стать `processing`; клиенту этот промежуточный ответ не отдаётся.
### 9.8. `message_attachments`
Поля: `id`, `dialog_id`, `message_id NULL`, `owner_user_id`, `direction` (`client_upload | company_inbound`), `original_file_name`, `safe_file_name`, `mime_type`, `size_bytes`, `checksum_sha256`, `scan_status`, `storage_bucket`, `object_key`, `quarantine_object_key NULL`, `upload_expires_at`, `completed_at`, common fields.
Поля: `id`, `dialog_id`, `message_id NULL`, `owner_user_id`, `direction` (`client_upload | company_inbound`), `original_file_name`, `safe_file_name`, `mime_type`, `size_bytes`, `checksum_sha256`, `scan_status`, `storage_bucket`, `object_key`, `quarantine_object_key NULL`, `quarantine_version_id NULL`, `quarantine_etag NULL`, `upload_expires_at`, `completed_at`, common fields.
Ограничения:
- `size_bytes > 0`;
- SHA-256 — 64 lowercase hex;
- scan: `pending | clean | infected | failed`;
- scan: `pending | clean | bypassed | infected | failed`; `bypassed` допустим только для file allow с `safety_processing_mode=mock`;
- до allow client file находится только в quarantine;
- attachment связывается максимум с одним message;
- для MVP у message максимум одно active attachment: unique partial `message_id`.
@@ -691,12 +692,15 @@ Reserved MVP table: `id`, `user_id`, `name`, `mime_type`, `size_bytes`, `checksu
- `message_id` UNIQUE;
- `attachment_id NULL`;
- `quarantine_object_key NULL`;
- `quarantine_version_id NULL`, `quarantine_etag NULL`;
- `task_location`;
- `processing_mode`, `config_version`, `rules_version NULL`, `last_poll_http_status NULL`;
- `status`: `polling | finalizing | completed | failed`;
- `deadline_at`, `next_poll_at`, `attempt_count`, `last_error_code`;
- `locked_at`, `locked_by`;
- timestamps.
Индексы: `(status, next_poll_at)`, `(deadline_at)`. Worker забирает `FOR UPDATE SKIP LOCKED`. Это не очередь анализа и не заменяет `message-safety`.
Индексы: `(status, next_poll_at)`, `(deadline_at)`. Worker забирает `FOR UPDATE SKIP LOCKED`. Это не очередь анализа и не заменяет `message-safety`. Checkpoints `completed|failed` удаляются через 7 дней; active checkpoints — только после terminal reconciliation.
### 9.11. `delivery_outbox`
@@ -747,7 +751,9 @@ Append-only: `id`, `event_type`, `actor_type`, `user_id NULL`, `ux_session_id NU
- `app_settings` — поля и правила из arch-04;
- `text_resources`: `id`, `mnemonic`, `locale`, `text_value`, `sort_order`, common fields; unique active `(mnemonic, locale)`;
- `popular_questions`: `id`, `mnemonic`, `locale`, `question_text`, `sort_order`, common fields; unique active `(mnemonic, locale)`;
- `sync_queue`, `entity_external_mapping` — shared contract с `bitrix-sync`.
- `sync_queue` — shared contract с `bitrix-sync`: migrations и trigger-функция принадлежат App DB/module-01, runtime claim выполняет `bitrix-sync` через минимальные GRANT.
- `bitrix_sync.entity_external_mapping` и rebind workflow принадлежат исключительно `bitrix-sync`; `api-backend` их не читает и не изменяет.
- существующие `han_app.entity_external_mapping`, `ClientProfile.bitrix_contact_id` и partial index удаляются expand/contract migration после переноса mapping и проверки отсутствия readers.
## 10. Alembic и транзакции
@@ -842,30 +848,36 @@ validate current consents and content union
enforce rate limits and idempotency
for file: lock attachment, require completed/pending and checksum equality
create Message(pending, accepted)
call POST /internal/safety/v1/messages/check
call POST /internal/safety/v2/messages/check
if 200 allow:
finalize_allow()
persist response.processing_mode, response.config_version
finalize_allow(processing_mode, config_version)
elif 403 deny:
finalize_deny()
elif 203 pending:
persist safety_tasks(task_id, deadline)
persist response.processing_mode, response.config_version
finalize_deny(processing_mode, config_version)
elif 202 pending:
persist safety_tasks(task_id, Location, deadline, processing_mode=standard, config_version)
while monotonic_now < request_deadline:
sleep(backoff_with_jitter)
poll GET /internal/safety/v1/messages/tasks/{task_id}
if 200 allow: finalize_allow() and return
if 403 deny: finalize_deny() and return
if 400 and code=stub_final_error and verdict=deny and details.terminal=true:
finalize_deny() and return
poll GET Location
if 200 allow: persist processing_mode/config_version; finalize_allow(...) and return
if 403 deny: persist processing_mode/config_version; finalize_deny(...) and return
if 503 and terminal=true and retryable=false:
mark failed and return 503
if other 4xx/5xx: return mapped dependency error
mark failed, retain checkpoint/quarantine
return 504
elif 409 and code=safety_request_conflict:
alert invariant violation, mark failed, return 500, do not repeat POST
else:
mark failed
return mapped dependency error
```
Polling interval начинается с `MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC`, допускает capped exponential backoff и jitter, но не превышает общий `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Клиенту не возвращается `203`. Terminal `400 stub_final_error` — намеренное test-only расширение `module-05`; оно преобразуется в публичный `422 message_blocked`, не считается infrastructure failure и не смешивается с malformed `400`.
Polling interval начинается с server `Retry-After`, допускает capped exponential backoff и jitter, но не превышает caller env `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Каждый v2 verdict/pending содержит `processing_mode` и `config_version`; MOCK возвращает только sync `200/403`. Клиенту internal mode/config/`202` не возвращаются: public POST сохраняет синхронную семантику. Legacy stub `/v1` с `203`/`stub_final_error` поддерживается только временным adapter-ом до cutover и не является target production path.
Capability snapshot `/health/ready` допускается кэшировать не дольше 5 с для fast-fail: file требует `files`, text с URL — `links`, text без URL — `text`. При `processing_mode=mock` normal capabilities имеют состояние `bypassed` и не применяются как fast-fail gate. Snapshot не является correctness gate: definitive capability повторно проверяет `POST /check`. При unavailable в standard mode api-backend возвращает public `503 dependency_unavailable`, не создаёт delivery outbox и не меняет status на blocked.
### 13.2. Final allow
@@ -874,13 +886,20 @@ Polling interval начинается с `MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC`
1. проверить checkpoint;
2. copy quarantine object в attachments bucket с conditional/idempotent key;
3. HeadObject destination, сверить checksum/size;
4. в транзакции изменить attachment на `clean`, storage location на S3-data; message на `allowed/accepted`; создать delivery outbox;
4. в транзакции изменить attachment на `clean` для standard mode или `bypassed` для MOCK, storage location на S3-data; message на `allowed/accepted` с фактическим `safety_processing_mode`; создать delivery outbox;
5. удалить quarantine object best-effort; при сбое cleanup повторит;
6. попытаться синхронно доставить outbox, чтобы исходный POST вернул финальный `delivered`.
### 13.3. Final deny
В транзакции: `blocked/rejected`, attachment `infected`; затем delete quarantine best-effort. В Open Lines ничего не отправляется. Realtime `message.status` публикуется, если message уже мог быть виден этому клиенту.
В одной транзакции:
1. client message → `blocked/rejected`, `text` заменяется пустой строкой/безопасным marker по M8;
2. attachment при наличии → `infected`;
3. создаётся ровно одна synthetic company-replica, связанная `related_message_id`, с `text` из active `text_resources(safety.chat.blocked, locale)`, `allowed/delivered`;
4. сохраняются только hash, internal rule/verdict/version в audit.
После commit quarantine удаляется best-effort. В Open Lines ничего не отправляется. Realtime публикует `message.status` исходного сообщения и `message.new` company-реплики. Public `422` содержит generic error envelope без `rule_id`.
### 13.4. Timeout/crash recovery
@@ -892,13 +911,13 @@ claim with lease
poll safety by task_id
if pending before recovery deadline: schedule next_poll_at
if allow: idempotent promote + delivery checkpoint
if deny (canonical 403 or test-only terminal 400): idempotent delete + reject
if deny (canonical 403): idempotent delete + reject + company replica
if budget exhausted: mark task failed, message failed, preserve audit
```
HTTP disconnect не отменяет durable recovery. Клиентский retry с тем же idempotency key получает восстановленный результат либо текущую dependency error. Recovery не принимает решение о типе анализа.
**Решение M5:** после client-facing timeout recovery budget продолжается ещё 15 минут как техническая константа модуля; до production-load test значение должно быть вынесено в infra env и добавлено в arch-04. Пока это **TBD-2**, код обязан иметь безопасный default и метрику.
**Решение M5:** `deadline_at = min(message_safety.expires_at, checkpoint.created_at + HAN_APP_SAFETY_RECOVERY_MAX_SEC)`, initial env = 1200 с. Client-facing wait остаётся 300 с; recovery продолжает без открытого клиентского соединения. Terminal Safety `503 retryable=false` немедленно завершает checkpoint как failed.
## 14. S3 attachment lifecycle
@@ -914,23 +933,23 @@ Lifecycle:
1. `init`: allow-list extension + declared MIME + size; create metadata; presign exact key, MIME, max size, TTL;
2. direct PUT client → S3-quarantine;
3. `complete`: HeadObject, size/MIME/checksum metadata; checksum при отсутствии trustworthy S3 checksum вычисляется safety service при scan;
3. `complete`: HeadObject конкретной version, size/MIME/server checksum; атомарно фиксирует `version_id + ETag + authoritative checksum`;
4. message send: attachment ownership/state/checksum;
5. allow: copy + verify + DB finalize + quarantine delete;
5. allow: conditional copy сохранённой source version с ETag/checksum match + verify + DB finalize + quarantine delete;
6. deny: quarantine delete + infected metadata;
7. abandoned/failed: cleanup only if expired and no active safety task;
7. abandoned/failed: cleanup через 48 ч, только если нет active safety task;
8. download: owner check → audit commit → short presigned GET.
Extension и MIME оба должны быть разрешены; server normalizes filename and sets safe `Content-Disposition`. S3 credentials never reach frontend.
**Допущение A3:** Selectel S3 может не предоставлять SHA-256 в `HeadObject`; `complete` сверяет клиентский checksum с signed metadata, а authoritative checksum подтверждает Message Safety. Если storage поддерживает checksum header, он обязателен.
Presigned PUT обязательно подписывает `If-None-Match: *`, checksum header и `Content-Type`; versioning quarantine включён. Повторный PUT того же key получает `412`. При отсутствии подтверждённой поддержки этих условий выбранным S3 adapter production upload блокируется, а не деградирует до overwrite.
Inbound operator file:
- validate count/size/MIME and URL scheme/host policy;
- protect against SSRF: no redirects to private/link-local ranges, DNS rebinding checks, max bytes streaming;
- download with timeout to temporary stream, never local persistent disk;
- optional antivirus policy; Message Safety outbound pipeline не вызывается;
- Message Safety/ClamAV не вызываются; остаточный malware-риск доверенного Bitrix24-channel принят для MVP;
- upload directly to S3-data attachments;
- only then atomically save attachment/message and ack inbox.
@@ -973,13 +992,19 @@ Outbox worker и synchronous first attempt используют один dispatc
Миграции создают triggers:
- insert active `UserIdentity`/`ClientProfile` без mapping → `contact.map_or_create`;
- изменение tracked profile/auth-phone fields`contact.update`;
- trigger строит deterministic dedup key;
- insert active `UserIdentity`/`ClientProfile` → coalesced `contact.map_or_create`; trigger не читает schema `bitrix_sync`, наличие mapping проверяет worker;
- фактическое изменение App-master `UserIdentity.phone_number``contact.update`;
- переход `UserIdentity` или `ClientProfile` из active в inactive/deleted → `contact.deactivate`;
- возврат active записи → coalesced `contact.map_or_create`;
- изменения CRM-master `full_name`, `citizenship`, `email` не создают App→CRM задачу;
- trigger проверяет значения через `IS DISTINCT FROM`, а не только факт присутствия колонки в `UPDATE OF`;
- trigger строит deterministic dedup key, уникальный только среди активных queue rows; завершённая/cancelled/dead-letter запись не блокирует новое событие;
- при `current_setting('han.sync_suppress', true)='true'` задача не создаётся;
- trigger и business update находятся в одной транзакции.
`bitrix-sync` получает ограниченные GRANT. Ошибка CRM не откатывает bootstrap и chat. Open Lines не зависит от CRM mapping.
`entity_id` всех contact-задач — `UserIdentity.id`; payload содержит только `schema_version`, `user_id`, безопасную причину и source timestamp, но не PII snapshot. Worker перечитывает актуальные identity/profile.
`bitrix-sync` получает ограниченные column/table GRANT, заданные module-07. Ошибка CRM не откатывает bootstrap и chat. Open Lines не зависит от CRM mapping.
## 17. Realtime
@@ -1197,6 +1222,8 @@ Open Lines недоступность отображается как component
**Решение M8:** blocked message text не нужен продукту после deny. В `messages.text` хранится пустая строка/безопасный redacted marker, а audit хранит только rule/verdict id и hash содержимого. Если регуляторно требуется исходный текст, это отдельное согласованное изменение retention/security.
Оценка monitor-only semantic rules выполняется контролируемой, аудируемой выборкой из App DB: доступ только у утверждённой роли, выборка ограничена по времени/объёму, purpose фиксируется в audit. Текст не копируется в schema/логи Message Safety; там остаются hash, `rule_id` и version.
## 24. Docker/runtime
Service compose:
@@ -1205,7 +1232,7 @@ Service compose:
- networks: `backend`, `observability`;
- env только через `${VAR}` из root `.env`;
- healthcheck `/health/live` для процесса; root orchestration учитывает readiness;
- depends_on health для Redis/Keycloak/message-safety, но приложение само retry startup dependencies;
- depends_on health только для локальных Redis/Keycloak; remote Message Safety не является Compose dependency и проверяется capability-aware на send path;
- managed PostgreSQL вне compose, TLS обязателен;
- stateless container, без persistent volume;
- init process для signal forwarding;
@@ -1251,7 +1278,7 @@ Frontend выполняет refresh token grant. API не обновляет tok
### 25.3. Файловое сообщение
create/reuse dialog → init → direct PUT quarantine → complete → send file message → safety `203` poll → allow promote → delivery outbox → Open Lines → delivered. При deny quarantine удаляется, Bitrix не вызывается.
create/reuse dialog → init → direct immutable PUT quarantine → complete with version/ETag/checksum → send file message → safety `202` poll → allow conditional promote → delivery outbox → Open Lines → delivered. При deny quarantine удаляется, Bitrix не вызывается.
### 25.4. Ответ оператора
@@ -1289,7 +1316,7 @@ Bitrix event → local app durable inbox → `POST /internal/openlines/v1/inbox`
### 26.3. Contract
- generated FastAPI OpenAPI matches committed `api-backend/openapi.yaml`;
- Message Safety POST `200/203/403`, task poll `203/200/403` и test-only terminal `400 stub_final_error`; проверены различение malformed `400` и mapping terminal `400` → public `422`;
- Message Safety v2 POST `200/202/403`, task poll `202/200/403`, terminal failed `503` и conflict `409`; legacy stub adapter тестируется отдельно до cutover;
- Open Lines message idempotency and inbox schemas;
- settings bridge DTO/token;
- common request-id/trace propagation;
@@ -1368,15 +1395,15 @@ Bitrix event → local app durable inbox → `POST /internal/openlines/v1/inbox`
- **A1:** locale API зарезервирован, MVP фактически `ru`.
- **A2:** inbox получит стабильный `event_id`; временно возможен deterministic fingerprint.
- **A3:** authoritative SHA-256 может подтверждаться Message Safety, если S3 HeadObject его не отдаёт.
- **A3:** production S3 adapter подтверждает signed checksum headers, versioning и conditional requests; иначе immutable upload не включается.
### Требуют согласования
- **TBD-1:** единый код для protected endpoint до bootstrap (`409` предложен).
- **TBD-2:** extended recovery budget после `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`.
- **Решение M5:** extended recovery ограничен `HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200` и Safety `expires_at`.
- **TBD-3:** добавить `event_id`/`occurred_at` в WS events и `event_id` в inbox OpenAPI.
- **TBD-4:** production SLO, RPS, concurrency, RPO/RTO и retention.
- **TBD-5:** antivirus policy для файлов оператора, которые не проходят outbound Message Safety.
- **Решение M9:** файлы оператора не проходят Message Safety/AV в MVP; только MIME/size/audit, residual malware risk принят.
- **TBD-6:** legal retention/erasure для PII, audit, blocked messages и S3-data.
- **TBD-7:** точный max WS connections/subscriptions/frame и queue size.
- **TBD-8:** G10 — окончательный DTO/mapping public app-config при оформлении OpenAPI.
+6 -6
View File
@@ -90,7 +90,7 @@ Resend запускает новое Keycloak action, блокирует double
- сообщения сортируются по `created_at asc`, дубли объединяются по `message_id`;
- `waiting_for_company`, `waiting_for_client`, `closed` отображаются русскими подписями;
- завершённая беседа readonly; CTA «Продолжить общение» повторно открывает текущий чат через `POST /dialogs`;
- промежуточный safety `203` клиенту не показывается: send request остаётся в progress до финального ответа.
- internal safety `202` клиенту не показывается: send request остаётся in progress до финального ответа.
### 5.4. Профиль
@@ -146,7 +146,7 @@ Bootstrap повторяем безопасно после неопределё
3. `POST /dialogs` с idempotency key, сохранить `dialog_id` и перейти на экран чата до отправки.
4. Экран чата выполняет `POST /dialogs/{id}/messages` с отдельным стабильным key.
5. Блокировать повторный click только для того же intent; другие действия не замораживать.
6. На `201` merge `MessageResponse`; на `422 message_blocked` не показывать красную техническую ошибку, а обновить историю с сохранённой backend `company`-репликой; на `503/504` предложить retry с тем же key.
6. На `201` merge `MessageResponse`; на `422 message_blocked` не показывать internal reason/rule и не строить собственный текст: получить/merge backend `company`-реплику (`message.new`) с контентом мнемоники `safety.chat.blocked`; на `503/504` предложить retry с тем же key.
7. Composer показывает счётчик `n/max`, где `max` приходит как `messages.max_text_length` из public app-config; сверх лимита отправка блокируется без обрезки ввода.
## 11. Файловый flow
@@ -156,11 +156,11 @@ MVP допускает ровно один файл, только allow-list ext
1. Локальная prevalidation.
2. Создать/reuse dialog.
3. `POST .../attachments/init` с filename, MIME, size.
4. Выполнить прямой `PUT upload_url` с точно выданными `upload_headers`; API domain при этом не используется.
5. Вычислить SHA-256, вызвать `complete`.
4. Вычислить SHA-256 до upload и выполнить прямой `PUT upload_url` с **точно** выданными `upload_headers`, включая `If-None-Match: *` и checksum; заголовки являются частью подписи.
5. Вызвать `complete` с тем же checksum; backend фиксирует authoritative S3 version/ETag/checksum.
6. Отправить file message с `attachment_id` и `sha256:<hex>`.
Presigned URL не сохраняется и редактируется из диагностик. Abort позволяет отменить PUT; orphan очищает backend. При expiry init выполняется повторно в рамках согласованного состояния. CORS S3 должен разрешать origin сайта, PUT и необходимые headers.
Presigned URL не сохраняется и редактируется из диагностик. `412` означает, что immutable key уже записан: frontend не повторяет PUT в тот же key, а запрашивает новый init. Abort позволяет отменить PUT; orphan очищает backend через 48 ч. При expiry init выполняется повторно в рамках согласованного состояния. CORS S3 разрешает origin сайта, PUT и только необходимые signed headers.
## 12. Realtime и polling
@@ -292,4 +292,4 @@ Browsers: Chromium, Firefox, WebKit; viewport desktop/mobile. В CI исполь
- F3: web storage policy refresh token перед production security review;
- F4: окончательный DTO app-config (G10);
- F5: WS `event_id`/protocol version (TBD module-01/G11);
- F6: продуктовые тексты всех error states по мнемоникам.
- F6 для Safety закрыт: generic deny использует `safety.chat.blocked`; остальные error-state мнемоники остаются в frontend content backlog.
+48 -14
View File
@@ -1,39 +1,68 @@
# module-03. Проектная спецификация корневого `nginx`
> Статус: целевая спецификация полностью рабочего edge-контура MVP.
> Статус: целевая спецификация независимых nginx-контуров ВМ1 и ВМ2.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
## 1. Назначение и обязательная топология
В production-like контуре существует ровно один корневой nginx. Только он публикует host-порты `80/443`, завершает TLS, раздаёт SPA и проксирует публичные маршруты. Контейнеры API, Keycloak, Redis, Safety, Bitrix и OTEL используют только `expose`/Docker networks.
В production-like контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle:
Если перед VM есть внешний WAF/LB, доверенные proxy CIDR задаются явно; nginx не доверяет произвольному `X-Forwarded-For`. Другой nginx на host не должен маршрутизировать сервисы по отдельности.
- nginx ВМ1 обслуживает frontend/API/auth/Open Lines/SMS;
- nginx ВМ2 напрямую обслуживает публичные CRM webhook `bitrix-sync` и private Message Safety API;
- публичный трафик ВМ2 не проходит через ВМ1;
- отказ или deploy ВМ1 не прерывает приём CRM webhook на ВМ2.
Контейнеры приложений не публикуют host ports. На каждой VM наружу смотрит только её nginx. Если перед конкретной VM есть WAF/LB, trusted proxy CIDR задаются отдельно.
## 2. Routing matrix
Порядок location критичен: exact/longest public routes до общего `/bitrix/`.
Порядок location критичен. Публичные route разделены по host/VM.
### ВМ1
| Внешний путь | Upstream | Режим |
|---|---|---|
| `/api/` | `api-backend:8000` | REST; `/api/v1/realtime` WS |
| `/auth/` | `keycloak:8080` | OIDC/OTP, prefix/hostname согласован с issuer |
| exact `/callbacks/idgtl/sms` | `sms-service:8080` | public HTTPS POST Direct; IP allowlist + Basic auth в upstream |
| `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS POST, no cache; отсутствует, пока действует stub module-07 |
| `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` | `bitrix-local-app:8080` | public HTTPS |
| exact `/health/live`, `/health/ready` | `bitrix-local-app:8080` | по умолчанию не публикуются; только при явно выбранной ops/monitoring policy |
| `/` | static SPA либо Expo dev upstream | `try_files` fallback |
Notification paths внутри `/api/` имеют отдельные edge-зоны: public catalog/campaigns, JWT read, actions, uploads и downloads. `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files запрещены exact prefix response `404` (допустим `403`, но единообразно выбран `404`). Никакого fallback internal path в SPA или общий proxy. `message-safety`, `/internal/sms/*` и `/internal/notifications/*` не имеют публичного route.
### ВМ2
| Внешний путь | Upstream | Режим |
|---|---|---|
| exact `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS Contact event; source IP CIDR/method/body/rate limits, query-token auth в upstream |
| exact `/bitrix/sync/webhook/alert` | `bitrix-sync:8080` | public HTTPS smart-process event; те же ограничения |
Notification paths внутри `/api/` ВМ1 имеют отдельные edge-зоны. На обоих public hosts `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files возвращают `404`; fallback на другую VM или SPA запрещён. До full sync cutover оба exact webhook route ВМ2 закрыты либо возвращают retryable `503`; успешный `2xx ignored` запрещён.
Query не участвует в exact location matching: URL штатного робота `/bitrix/sync/webhook/<type>?token=...&ID=...` попадает в соответствующий exact route. До proxy nginx проверяет непосредственный source IP по version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`; пустой/невалидный список при enabled receiver блокирует deployment. Адрес из недоверенного `X-Forwarded-For` не используется. При внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP.
Запрос вне allow-list получает generic `403` без proxy. В безопасном журнале с ограниченным retention сохраняются только timestamp, source IP, route class и outcome; query/body не сохраняются. Telemetry pipeline экспортирует `webhook_rejected_total{receiver,reason="source_ip"}` без IP label. Allow-list не расширяется автоматически: всплеск Contact, восстановленных инкрементальной reconciliation, инициирует проверку rejected-IP журнала, подтверждение принадлежности адреса Битрикс24 и reviewed reload конфигурации.
## 3. Upstreams
Именованные upstream: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, `bitrix_sync`, опционально `frontend_dev`. Для одной replica допустим `server service:port`; `keepalive` включён. Docker DNS resolver задаётся с коротким `valid` и `resolve` там, где поддерживает выбранная nginx edition; иначе контейнер перезапускается при смене IP upstream.
Именованные upstream ВМ1: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, опционально `frontend_dev`, а также private `processing_gateway` только для вызовов Message Safety из api-backend.
Nginx ВМ2 имеет независимые server blocks:
- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook;
- private `8443` с сертификатом internal CA: только server-to-server Message Safety и approved ops.
| Path | Local upstream | Caller |
|---|---|---|
| `/internal/safety/v2/*` | `message-safety-api:8080` | api-backend ВМ1 |
| `/internal/sync/v1/*` | `bitrix-sync:8080` | ops/allow-listed service |
Public и private server blocks не имеют общего fallback. Все прочие paths/methods возвращают `404/405`. Private listener доверяет forwarded headers только от allow-listed private caller; public listener применяет собственную trusted proxy policy.
Upstream failures не перенаправляются на другой сервис и не попадают в SPA. API возвращает `502/504` с безопасным nginx body и `X-Request-ID`; custom JSON error допустим для `/api`, но не имитирует backend domain code.
## 4. HTTP/HTTPS и TLS
- единый web host `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`;
- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`; исключение — `/bitrix/sync/webhook/contact|alert`, которые возвращают generic `404/426` без redirect и отражения query token;
- выделенный API host, если появится, не имеет listener `:80`;
- `:443 ssl http2`, TLS 1.2/1.3, современные cipher suites, session tickets по ops policy;
- сертификат доверенного CA, private key read-only и недоступен приложению;
@@ -134,7 +163,7 @@ traceparent: входной валидный либо новый согласн
- `ws_connect`: handshake;
- `connections`: `limit_conn`.
Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. OPTIONS не должен расходовать auth budget чрезмерно. Bitrix webhook retries имеют отдельный достаточный burst и всё равно проверяют application token в сервисе.
Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. OPTIONS не должен расходовать auth budget чрезмерно. Bitrix webhook retries имеют отдельный достаточный burst, проходят source IP allow-list и проверяют query receiver token в сервисе.
## 10. Static SPA и dev mode
@@ -195,7 +224,7 @@ Bitrix placement может требовать embedding: для exact `/bitrix/
## 14. Логи и OTEL correlation
JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI без sensitive query, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention.
JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI на основе `$uri` без `$request_uri`/`$args`, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention.
Не логируются Authorization, cookies, request/response body, OTP, tokens, query token, presigned query, PII. Error log структурирован настолько, насколько позволяет nginx; debug выключен production.
@@ -253,7 +282,7 @@ docker compose exec nginx nginx -t
curl -I http://tohin.ru/
curl -vk https://tohin.ru/api/v1/public/app-config
openssl s_client -connect tohin.ru:443 -servername tohin.ru
curl -i https://tohin.ru/internal/safety/v1/messages/check
curl -i https://tohin.ru/internal/safety/v2/messages/check
```
Автоматические тесты:
@@ -271,15 +300,20 @@ curl -i https://tohin.ru/internal/safety/v1/messages/check
- CSP/CORS preflight и Bitrix placement exception;
- upstream down/timeout, failed reload, renewal rehearsal;
- logs не содержат secrets/query tokens.
- CRM webhook exact routes принимают query без изменения location matching; allowed source IP проксируется, wrong IP получает `403` до upstream;
- HTTP webhook URL с query token не перенаправляется на HTTPS и не отражает query в `Location`/error;
- source-IP rejects попадают в безопасный bounded-retention журнал и low-cardinality telemetry без query/body/IP label;
- allowed Direct callback проходит; wrong IP/method и любой `/internal/sms/*` отклоняются; Authorization отсутствует в логах.
- `/internal/notifications/*` снаружи всегда `404`; notification read/action/upload/public routes используют свои зоны и возвращают `429`.
- CSP содержит `frame-src 'none'`; инструкция проверяется как новая вкладка без embedded content.
## 19. Definition of Done
- единственный root nginx публикует только 80/443;
- на каждой VM ровно один nginx; ВМ1 и ВМ2 независимо публикуют только свои утверждённые `80/443`, ВМ2 дополнительно слушает private `8443`;
- TLS/ACME bootstrap, renewal и safe reload испытаны;
- routing matrix и route precedence покрыты;
- CRM webhook достигает ВМ2 напрямую и продолжает приниматься при остановленном nginx ВМ1;
- CRM webhook ограничен version-controlled source IP CIDR allow-list; query token и form body отсутствуют в access/error logs и traces;
- internal endpoints/ports извне недоступны;
- WS работает на `/api/v1/realtime`;
- message timeout равен safety max + минимум 30 секунд;
@@ -293,8 +327,8 @@ curl -i https://tohin.ru/internal/safety/v1/messages/check
## 20. Решения, допущения и TBD
**Решения:** один nginx; njs/module для UUID; internal → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints.
**Решения:** независимые public nginx ВМ1/ВМ2; CRM webhook приходит прямо на ВМ2 через source IP CIDR allow-list; private `8443` ВМ2 используется только server-to-server; njs/module для UUID; public internal paths → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints.
**Допущения:** MVP использует единый host `tohin.ru`; upstream service names стабильны в Compose; S3 CORS настраивается отдельно.
**Допущения:** ВМ1 и ВМ2 используют разные public hosts и сертификаты; upstream service names стабильны внутри каждого Compose; S3 CORS настраивается отдельно.
**TBD:** N1 доверенные WAF CIDR; N2 production cipher suite/OCSP; N3 нужен ли публичный health; N4 точный CSP Expo build; N5 Bitrix frame ancestor domains; N6 финальные burst/connection limits; N7 certbot vs другой ACME client после ops review.
+31 -26
View File
@@ -1,15 +1,15 @@
# module-04. Проектная спецификация Redis
> Статус: целевая спецификация Redis в едином Docker Compose MVP.
> Статус: целевая спецификация Redis для двух Compose-контуров; legacy DB2 stub описан только до cutover.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
## 1. Назначение и инварианты
Один Redis-контейнер предоставляет быстрые ephemeral функции трём логическим DB:
Redis разделён по deployment/security boundary:
- DB0 `api-backend`: idempotency fast layer и API rate limits;
- DB1 — realtime и coordination;
- DB2 — `message-safety` stub tasks/cache.
- Redis ВМ1: DB0 (`api-backend` idempotency/rate) и DB1 (realtime/coordination);
- Redis Safety ВМ2: отдельный instance для hot cache, rate limiting и optional worker wake-up;
- legacy DB2 ВМ1 существует только для test stub v1 до cutover и после него удаляется.
Redis не является бизнес-очередью, source of truth сообщений, sync tasks, audit, профилей или delivery checkpoint. Надёжные состояния остаются в managed PostgreSQL/S3. Потеря Redis может ухудшить сервис, но не должна создавать потерю подтверждённых сообщений либо дубль side effect: durable idempotency/outbox/checkpoint api-backend описаны в module-01.
@@ -17,9 +17,9 @@ OTP counters api-backend в Redis не хранит; они принадлежа
## 2. Версия и topology
Redis 7.x, image закреплён по digest. Одна primary instance на VM без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды.
Redis 7.x, image закреплён по digest. На каждой VM одна нужная primary instance без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды.
Logical DB — изоляция имён, не security boundary и не независимый memory quota. При росте или разных eviction/SLA DB2 и DB0 выносятся в отдельные instances.
Logical DB — изоляция имён, не security boundary. Safety уже вынесен в отдельный instance ВМ2; DB0/DB1 остаются на ВМ1.
## 3. Общие правила ключей
@@ -81,18 +81,20 @@ Redis Streams не используются как бизнес queue. Если
Acquire: `SET key owner NX PX ttl`; extend/release — Lua compare owner. Worker обязан опираться также на PostgreSQL row lease/`FOR UPDATE SKIP LOCKED`; Redis lock — оптимизация, не единственная защита. Fencing token рекомендуется для внешнего side effect, а уникальные DB constraints/idempotency остаются финальной защитой.
## 8. DB2: Message Safety stub
## 8. Redis Safety ВМ2
| Key | Тип/value | TTL |
|---|---|---|
| `han:safety:task:{task_id}` | HASH/JSON v1: created, polls, optional seed/context | `MESSAGE_SAFETY_TASK_TTL_SEC` |
| `han:safety:tasklock:{task_id}` | owner token | 530s |
| `han:safety:rl:service:{caller}:{window}` | counter | window+jitter |
| `han:safety:verdict:{content_hash}:{rules_version}` | optional cache | bounded technical TTL |
| `han:safety:text:{analysis_hash}:{rules_version}` | hot text-rules result, monitor rule ids без raw text | active config, seed ≤48h |
| `han:safety:verdict:{content_hash}:{config_version}:{detector_bundle}` | hot file verdict cache | active config, seed ≤30d |
| `han:safety:link:{url_hash}:{rules_version}:{config_version}` | stable local policy cache | active config, seed ≤48h |
| `han:safety:dns:{host_hash}:{rrtype}` | DNS answer; classification повторяется под текущей policy | actual TTL, active hard max seed 900s |
| `han:safety:wakeup` | Pub/Sub notification only | no storage |
Для требуемой заглушки task — ephemeral contract state. Истечение task возвращает безопасный `404 task_not_found/expired` по internal error semantics. В production safety authoritative audit/cache может находиться в PostgreSQL `message_safety`; Redis DB2 не заменяет его.
PostgreSQL `message_safety.safety_tasks` — единственный queue/lease source (`FOR UPDATE SKIP LOCKED`, fencing generation). Redis не хранит authoritative task state, locks или leases. Cache loss/restart безопасно восстанавливается из PostgreSQL; Redis outage не выключает core Safety.
Random verdict каждого GET по заданию независим; Redis хранит существование/TTL и счётчик polls для observability, но не предопределяет финал. В deterministic tests seed/RNG injected на уровне сервиса.
Legacy v1 stub может временно использовать DB2 ВМ1 для random task state. Этот namespace не используется production v2 и удаляется вместе со stub.
## 9. Serialization и limits
@@ -112,8 +114,10 @@ Random verdict каждого GET по заданию независим; Redis
| rate limit | window + 1030% deterministic jitter |
| realtime connection | 90s; set membership 120s |
| coordination lock | 30s |
| safety task | default 15m, обязательно > API poll max 300s + recovery margin |
| safety cache | default 560m по rules version |
| safety file hot cache | ≤30d; authoritative row/version в PostgreSQL |
| safety text-rules cache | 48h; invalidation by rules version |
| safety stable link policy cache | 48h |
| safety DNS cache | actual DNS TTL, hard max 900s |
Новый key без TTL запрещён contract test, кроме Pub/Sub channel (не key) и ops metadata с явным обоснованием.
@@ -155,18 +159,18 @@ Eviction MVP: `volatile-lru`/`volatile-ttl`, так как все application ke
DB0 rate = peak identities × routes × active windows × bytes/key
DB0 idem = mutating requests/24h × avg sanitized record
DB1 = peak connections × connection metadata + Pub/Sub buffers
DB2 = safety tasks within TTL × avg task metadata
total × 1.5 allocator/fragmentation × 1.3 growth reserve
Redis Safety = hot verdict/link/DNS entries + rate windows + Pub/Sub buffers
each instance total × 1.5 allocator/fragmentation × 1.3 growth reserve
```
Pub/Sub output buffers и slow consumers имеют hard/soft limits. Load test фиксирует peak RPS, WS connections, idempotency response size и AOF rewrite headroom.
## 15. Auth, ACL и network boundary
Redis не публикует `6379` на host, подключён только к Docker `backend`. `protected-mode yes`, bind container interface, default user отключён. ACL users:
Оба Redis не публикуют `6379` на host и подключены только к local Docker `backend` своей VM. `protected-mode yes`, default user отключён. ACL users:
- `api_backend`: DB0/DB1 key prefixes, нужные command categories;
- `message_safety`: только DB2 prefixes;
- `message_safety`: только Redis Safety prefixes;
- `ops_health`: `PING`, ограниченный `INFO`;
Важно: Redis ACL не ограничивает logical DB напрямую надёжно; key-prefix patterns и разные credentials обязательны. `SELECT` запрещается, клиент URL сразу задаёт DB, но ACL prefix остаётся основной защитой.
@@ -195,10 +199,10 @@ URL:
```text
REDIS_URL=redis://api_backend:<secret>@redis:6379/0
REDIS_REALTIME_URL=redis://api_backend:<secret>@redis:6379/1
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/0
```
Добавление credential env требует обновления arch-04 `.env.example`; до этого имена credential variables — TBD, URL может содержать injected secret.
Первые два URL существуют только на ВМ1. На ВМ2 `MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/0`; credential доставляется secret file и не входит в общий `.env`.
## 17. Health и degraded behavior
@@ -211,7 +215,8 @@ MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
- profile/history GET могут работать под edge limits;
- public GET использует bounded local conservative limiter/cache;
- realtime cross-instance publish/coordination деградирует; REST/polling остаётся source of truth;
- safety stub для digit task не может гарантировать GET task state — check возвращает `503`, а существующие task GET — `503`; синхронные text allow/deny могут работать только если policy явно разрешает Redis-independent path;
- production Safety продолжает task claim/poll через PostgreSQL; hot cache/rate/wakeup деградируют и прогреваются после восстановления Redis;
- legacy stub v1 может стать недоступным при потере своей DB2 до cutover;
- internal inbox не теряется из-за Redis, так как durable receipt в PostgreSQL.
При latency выше threshold clients используют short timeout/circuit, не создают бесконечные retry storms. Reconnect — exponential backoff+jitter.
@@ -224,7 +229,7 @@ Redis backup не используется для бизнес restore. Runbook:
2. при целостном AOF/RDB восстановить на отдельном instance и проверить;
3. иначе поднять пустой Redis;
4. api-backend прогревает idempotency по durable records, realtime восстанавливается reconnect/polling;
5. незавершённые safety tasks обрабатываются по service semantics/expire; api-backend durable `safety_tasks` сообщает dependency error/recovery.
5. production safety tasks продолжают обрабатываться из PostgreSQL; Redis Safety прогревается лениво.
Не копировать Redis dump в небезопасное место: keys содержат UUID и hashed identifiers.
@@ -240,7 +245,7 @@ Redis backup не используется для бизнес restore. Runbook:
- script errors/NOSCRIPT/slowlog;
- rate limit decisions, idempotency hit/conflict/fallback;
- Pub/Sub subscribers/output buffer/slow disconnect;
- safety task create/get/expire.
- Safety hot-cache hit/miss, DNS TTL cap и wakeup subscribers.
Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF error, no recent persistence, client buffer pressure, unexpected keys without TTL.
@@ -252,7 +257,7 @@ Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF e
- idempotency same/different fingerprint, lock ownership, expiry, Redis loss + PostgreSQL fallback;
- realtime heartbeat cleanup, duplicate disconnect, Pub/Sub loss + REST recovery;
- locks expiry/late owner/fencing;
- safety task TTL, concurrent polls и missing task;
- Safety cache loss/rebuild, DNS TTL cap и доказательство отсутствия task/lease state в Redis;
- `NOSCRIPT` reload;
- all application keys имеют TTL;
- max value/invalid serialization;
@@ -279,6 +284,6 @@ Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF e
**Решения:** один instance/три DB MVP; AOF everysec + RDB; Pub/Sub best effort; PostgreSQL durable fallback; prefix ACL; все application keys с TTL.
**Допущения:** одна VM и одна replica API на старте; Redis loss допустим без потери business truth.
**Допущения:** по одной Redis instance на ВМ1/ВМ2 и одна Safety API replica на старте; Redis loss допустим без потери business truth.
**TBD:** R1 точный maxmemory после load profile; R2 eviction policy после измерений; R3 credential env names в arch-04; R4 Safety task TTL/recovery margin; R5 TLS при изменении network topology; R6 момент разделения DB на instances; R7 RPO/RTO ops target.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -640,4 +640,4 @@ Business settings здесь не хранятся. Legacy `BITRIX_SYNC_FORWARD_
- B-TBD4: retention/RPO/RTO и DLQ replay authorization.
- B-TBD5: encryption key source/rotation runbook до production.
- B-TBD6: точный CSP `frame-ancestors` placement.
- B-TBD7: antivirus policy operator files остаётся у `api-backend`.
- B7: operator files считаются trusted-channel данными MVP; api-backend применяет MIME/size/audit без Message Safety/AV, residual malware risk принят.
File diff suppressed because it is too large Load Diff
+19 -10
View File
@@ -1,6 +1,6 @@
# module-09. Наблюдаемость production-like контура
> Статус: целевая спецификация наблюдаемости MVP на одной VM.
> Статус: целевая спецификация наблюдаемости MVP для ВМ1/ВМ2 и отдельного private SigNoz.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md)[`module-08-keycloak.md`](module-08-keycloak.md).
## 1. Цели и границы
@@ -20,11 +20,11 @@ Telemetry не является источником бизнес-истины
### 2.1. Обязательный минимум в основном Compose
Архитектура явно требует только `otel-collector`. Поэтому **основной production-like Compose обязан содержать Collector, но не обязан размещать Prometheus/Grafana/Loki/Tempo на той же VM**.
Каждый root Compose (ВМ1 и ВМ2) обязан содержать локальный `otel-collector`. Prometheus/Grafana/Loki/Tempo не обязаны размещаться на application VM.
Предпочтительный operable-вариант после выбора backend:
1. приложения экспортируют OTLP gRPC в `otel-collector:4317`;
1. приложения на каждой VM экспортируют OTLP gRPC только в свой local `otel-collector:4317`;
2. Collector отправляет telemetry в выбранный удалённый управляемый OTLP backend провайдера;
3. JSON stdout остаётся аварийным локальным журналом Docker с rotation;
4. пока удалённый backend не выбран, допустим архитектурный минимум из arch-03: bounded JSON stdout/platform logs и Collector `debug` exporter с sampling в acceptance; такой режим не считается полноценным production-хранением и не закрывает alerting/SLO.
@@ -309,6 +309,7 @@ CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, D
### Message Safety
- checks/verdicts по `allow|deny|pending|error`;
- `processing_mode`, active/used `config_version`, config activation result, `message_safety_mock_enabled` и forced outcomes по `text|file`/`allow|deny`;
- poll duration/count buckets, timeout и recovery backlog age;
- stub mode info и terminal `400` отдельно, пока действует test-only контракт;
- cache hit, Redis latency, task expired/not-found.
@@ -342,11 +343,11 @@ CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, D
## 10. Dashboards
1. **Executive/SLO**: availability, error budget burn, p50/p95/p99, message delivery, auth, active incidents.
2. **nginx edge**: RPS, 4xx/5xx, upstream latency/status, 429, WS, TLS, cache.
2. **nginx ingress по VM**: отдельные панели/filters ВМ1 и ВМ2; RPS, 4xx/5xx, upstream latency/status, 429, WS, TLS, cache, CRM webhook на ВМ2.
3. **api-backend**: routes, DB/Redis pools, JWKS, circuits, outbox/safety backlog, S3.
4. **message-safety**: verdicts, pending/poll, task TTL, Redis, distribution stub outcomes.
4. **message-safety**: capabilities, verdicts, `202` poll, PG queue age/leases/fencing, ClamAV/signature age, file/link cache и DNS dependency.
5. **bitrix-local-app**: install/OAuth, connector, outbound/inbox/DLQ, API/Bitrix latency.
6. **bitrix-sync**: mode, DB probe, last success/staleness; нельзя показывать CRM sync как рабочий в stub.
6. **bitrix-sync**: mode/readiness, queue depth/oldest age, workflow/command transitions, CRM batch latency/subcommand outcome, limiter/throttle, retry/DLQ, webhook/reconciliation lag, mapping invariants и business-alert SLA.
7. **Keycloak**: login/OTP/lockout, sessions/tokens, provider settings cache, JVM/DB.
8. **Redis**: memory/evictions/AOF/latency/clients/keyspace.
9. **PostgreSQL/S3**: provider metrics, storage, connections, backup/PITR, object errors.
@@ -386,12 +387,18 @@ CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, D
- TLS expiry <14 дней warning, <7 дней page;
- disk >85% warning, >92% page; OOM/restart loop;
- managed PG backup/PITR failure.
- Message Safety active config missing/invalid, referenced artifact unavailable или config refresh stale >5 с.
- `message_safety_mock_enabled=1` — active page/high-severity alert без auto-resolve по времени; закрывается только после возврата в standard.
- bitrix-sync invalid/revoked credential или обязательная configuration/grant missing;
- bitrix-sync technical DLQ >0, mapping invariant violation или worker/limiter heartbeat stale;
- bitrix-sync queue oldest age >30 с при healthy CRM либо sustained рост;
- Contact webhook/reconciliation cursor lag выше двух configured intervals.
### Ticket/warning alerts
- p95 regression 20% release-over-release;
- settings/JWKS cache stale;
- bitrix-sync stub probe stale >150s;
- bitrix-sync CRM last success stale, rate-limit errors выше baseline или business alert SLA overdue;
- OAuth expires <24h без успешного refresh;
- quarantine orphan growth;
- cardinality/ingest growth >2× baseline.
@@ -453,6 +460,8 @@ Legal retention/erasure имеет приоритет; изменение тре
- restart policy с backoff;
- приложения имеют bounded non-blocking OTLP exporter queue.
Collector существует отдельным экземпляром на ВМ1 и ВМ2. Каждый имеет собственный `otel-queue` volume/limits и экспортирует в private SigNoz `192.168.0.5:4317`; ВМ2 никогда не использует Docker hostname collector ВМ1. Telemetry outage/overflow fail-open для business и Safety readiness, но создаёт alert.
Доступ к Docker socket запрещён. Для container metrics используется безопасный exporter/hostmetrics, а не unrestricted socket mount.
## 15. Security
@@ -521,7 +530,7 @@ Legal retention/erasure имеет приоритет; изменение тре
## 17. Проверки и Definition of Done
- Collector config проходит validate и запускается в едином Compose;
- Collector config проходит validate и запускается в root Compose каждой VM;
- OTLP gRPC и HTTP принимают три сигнала;
- все сервисы имеют правильные resource attributes;
- request проходит nginx/API/Safety/Open Lines с одним `request_id` и связанным trace;
@@ -541,7 +550,7 @@ Legal retention/erasure имеет приоритет; изменение тре
### Решения
- O1: обязательный архитектурный минимум — Collector; выбран self-hosted SigNoz
- O1: обязательный архитектурный минимум — local Collector на каждой application VM; выбран self-hosted SigNoz
на отдельной VM `192.168.0.5`, доступный по приватному OTLP gRPC.
- O2: Prometheus/Grafana/Loki/Tempo — отдельный operable profile, не скрытая обязательная нагрузка основной VM.
- O3: JSON stdout — аварийный локальный buffer; audit App DB — durable.
@@ -562,7 +571,7 @@ Legal retention/erasure имеет приоритет; изменение тре
1. `arch-03` разрешает stdout/platform exporter как минимум, но production-like расследования и alerts без backend ограничены. Здесь remote OTLP backend рекомендован, но не объявлен выбранным: провайдер остаётся TBD.
2. `module-05` использует test-only terminal `400` и non-sticky verdict вместо canonical `403`/sticky production verdict. Dashboards обязаны маркировать сервис `stub`; production SLO Safety на нём недостоверен.
3. `module-07` — только DB connectivity stub, тогда как arch-01/02 описывают полноценную CRM sync. Dashboard не должен показывать queue/CRM SLI, которых нет.
3. `module-07` задаёт full sync target; до code/portal cutover dashboard обязан показывать `sync_disabled`, а не синтетический CRM success. Queue/CRM SLI включаются только после module-07 preflight.
4. Retention, RPO/RTO и production SLO открыты в module-01/04/06/08; значения этого документа являются initial ops policy, не закрывают legal/product TBD.
5. Observability env (`OTEL_REMOTE_*`, service names, sampling/queue limits)
добавлены в arch-04 и `.env.example`; sampling/queue limits уточняются после
+189 -75
View File
@@ -1,34 +1,47 @@
# module-10. Runbook развёртывания HAN Chat
> Статус: последовательная инструкция первого production-like деплоя и эксплуатации на одной Ubuntu VM.
> Статус: целевой runbook ВМ1/ВМ2. Команды существующего stub-контура применимы только до production Safety cutover и явно отмечены как legacy.
> Все значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders. Команды с `cd <BACKEND_ROOT>` требуют подстановки реального пути корня backend-репозитория на VM.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md)[`module-09-observability.md`](module-09-observability.md).
> Канонические источники: [`../architectory/README.md`](../architectory/README.md), [`../architectory/arch-00-glossary.md`](../architectory/arch-00-glossary.md), [`../architectory/arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`../architectory/arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`../architectory/arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md), [`../architectory/arch-04-settings-and-content.md`](../architectory/arch-04-settings-and-content.md), [`../architectory/arch-05-agent-development-process.md`](../architectory/arch-05-agent-development-process.md), [`../architectory/arch-06-service-hosting-security.md`](../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md)[`module-09-observability.md`](module-09-observability.md).
## 1. Неподвижные правила
1. Один root `docker compose` запускается из `<BACKEND_ROOT>`.
2. Ровно один edge nginx публикует `80/443`.
3. API, Keycloak, Redis, OTEL, Safety и Bitrix-сервисы не имеют host `ports`.
1. Один root Compose project описывается в `<BACKEND_ROOT>`; в steady state его запускает root-owned systemd-unit/helper, а не пользователь из группы `docker`.
2. На каждой VM ровно один nginx; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress и deployment lifecycle.
3. Application containers не имеют public host ports. Nginx ВМ1 публикует свой `80/443`; nginx ВМ2 — отдельный `80/443` только для exact CRM webhook и private `8443` для Message Safety/internal access.
4. `/internal/*` не маршрутизируется публично.
5. Managed PostgreSQL находится вне Compose, в той же VPC, без public IP.
6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL.
7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты.
8. Миграции выполняются отдельными one-shot steps до новой версии приложения.
9. Message Safety запускается как documented stub до замены; это не production antivirus/moderation.
10. `bitrix-sync` запускается как DB-connectivity stub; полноценной CRM sync нет.
10. `bitrix-sync` вводится только после выполнения preflight/cutover gates module-07; до этого `BITRIX_SYNC_ENABLED=false`, public webhook закрыт на edge.
11. На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо.
11. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files.
12. OS-роли, SSH/sudo, secrets delivery, container hardening и private-VM lockdown подчиняются arch-06.
Прямые `docker compose` команды в этом runbook выполняются `admin` только при bootstrap/recovery либо инкапсулируются в утверждённые root-owned systemd-units. Они не являются основанием выдавать `deploy` доступ к Docker daemon.
## 2. Роли и обозначения
- **Cloud admin**: VPC, VM, PG, S3, DNS/security groups.
- **Deploy operator**: VM, Compose, migrations, release/rollback.
- **Deploy operator / OS user `deploy`**: запуск утверждённых release/rollback/migration systemd-units и root-owned Message Safety mode helper; без группы `docker`, записи в production compose/unit/scripts/config и общего sudo.
- **Break-glass OS user `admin`**: bootstrap и аварийное восстановление; не используется для штатного деплоя.
- **OS user `tunnel`**: только allow-listed local TCP forwarding к private endpoints; без sudo/shell operations.
- **Bitrix admin**: local app, connector, Open Line 8, callbacks.
- **Security owner**: secrets, Keycloak admin MFA, firewall, retention.
- **Safety Service Owner**: API/data contract, capacity result и v2 cutover/rollback sign-off.
- **Rule Pack Owner**: rules bundle, corpus, monitor report и version release.
- **Product Owner**: mnemonic `safety.chat.blocked` и business acceptance chat flow.
- **Operations Owner**: VM2 alerts, ClamAV signatures, incident/reprovision/restore rehearsal.
Placeholders:
```text
<PUBLIC_HOST> например chat.example.ru
<VM_PUBLIC_IP> публичный IPv4 VM
<PROCESSING_PUBLIC_HOST> отдельный public host ВМ2, например processing.example.ru
<VM2_PUBLIC_IP> публичный IPv4/LB address ВМ2
<VM_PRIVATE_IP> приватный IPv4 VM
<VPC_CIDR> например 10.20.0.0/24
<PG_PRIVATE_HOST> private FQDN/IP managed PG
@@ -58,8 +71,9 @@ Placeholders:
- remote observability backend;
- RPO/RTO и maintenance window;
- ответственных за alerts/Bitrix/Keycloak.
- назначенные Safety Service/Rule Pack/Product/Security/Operations owners и approvals cutover.
Начальный sizing без local Grafana stack:
Начальный sizing ВМ2 без local Grafana stack:
- VM: 4 vCPU, 8 ГБ RAM, 80 ГБ SSD, 4 ГБ swap;
- managed PG: минимум 2 vCPU, 4 ГБ RAM, 50 ГБ, HA по возможности;
@@ -67,7 +81,16 @@ Placeholders:
- OTEL Collector: 512 МиБ + 510 ГБ queue;
- свободный диск VM после pull/build: не менее 30%.
Это baseline, не гарантия. До real traffic обязателен load test с long Safety poll и WS.
Это baseline, не гарантия. До real traffic обязателен load test module-05 §15.4:
- sustained 10 text checks/s: p95 ≤2 с, p99 ≤5 с;
- sustained 2 file checks/s на 5 worker slots: среднее processing ≤2.5 с, p95 ≤60 с, public wait ≤300 с;
- 100 pending принимаются; 101-й file POST получает retryable `503` без новой task;
- RPS overflow даёт `429 + Retry-After`;
- long Safety poll не блокирует WS/read API;
- если gate не пройден, увеличить slots/CPU/clamd scan lanes и повторить; production traffic не открывать.
Monthly availability SLO для MVP не задаётся; это не отменяет latency/load gates и alerts.
### Gate 0
@@ -83,30 +106,37 @@ Placeholders:
### 4.1. Сеть
Создать одну private subnet для VM и managed PG. PG получает только private address. VM имеет public IP только для nginx/SSH.
Создать private subnet для ВМ1, ВМ2, SigNoz и managed PG. ВМ1 и ВМ2 имеют отдельные public IP/LB только для своих nginx; service-to-service и PostgreSQL traffic остаётся private. SSH к обеим VM — только ops VPN/bastion.
Security groups:
| Source | Destination | Port | Rule |
|---|---|---:|---|
| trusted ops CIDR/VPN | VM | SSH `<SSH_PORT>` | allow |
| internet | VM | TCP 80 | allow для redirect/ACME |
| internet | VM | TCP 443 | allow |
| VM private IP/SG | managed PG | `<PG_PORT>` | allow |
| VM | internet | 443 | allow egress: registry, Bitrix, S3, OTLP, ACME, i-Digital Direct |
| trusted ops CIDR/VPN | ВМ1, ВМ2 | SSH `<SSH_PORT>` | allow |
| internet | ВМ1 | TCP 80/443 | allow edge redirect/ACME/application |
| internet | nginx ВМ2 | TCP 80/443 | allow ACME/redirect + exact CRM webhook |
| ВМ1 SG | ВМ2 | TCP 8443 | private TLS only |
| ВМ1/ВМ2 service SG | managed PG | `<PG_PORT>` | allow по нужным DB roles |
| ВМ2 collector | private SigNoz | TCP 4317 | allow |
| ВМ2 workers | S3 endpoints | TCP 443 | allow |
| ВМ2 `bitrix-sync` | approved Bitrix portal | TCP 443 | allow |
| ВМ2 `freshclam` | approved signature CDN | TCP 443/80 по vendor manifest | allow |
| ВМ2 | trusted DNS/NTP | UDP/TCP 53, UDP 123 | allow |
| internet | managed PG | any | deny |
| internet | VM | 6379, 4317, 4318, 8000, 8080, 9000 | deny |
| internet | ВМ2 | any кроме nginx 80/443 | deny ingress |
| internet | обе VM | 6379, 4317, 4318, 8000, 8080, 8443, 9000 | deny public |
Если cloud SG не поддерживает egress allow-list, оставить egress open и контролировать destinations приложением/TLS; не ломать S3/Bitrix/OIDC.
ВМ2 использует default-deny egress. Registry/OS repositories открываются только в bootstrap/controlled window и затем снова закрываются. Если provider SG не умеет destination allow-list, применяется host firewall/proxy/NAT policy; постоянный open egress для ВМ2 не является допустимым production состоянием.
### 4.2. DNS
Создать `A <PUBLIC_HOST> → <VM_PUBLIC_IP>`. Не добавлять `www`, если он не нужен и не включён в certificate. Для отдельного API host действуют правила arch-03; MVP предпочтительно использует один host с paths.
Создать `A <PUBLIC_HOST> → <VM1_PUBLIC_IP>`, `A <PROCESSING_PUBLIC_HOST> → <VM2_PUBLIC_IP>` и private DNS `processing.internal → <VM2_PRIVATE_IP>`. Public host ВМ2 используется только CRM webhook; private name не публикуется во внешнем DNS.
Проверка с рабочей станции:
```bash
dig +short <PUBLIC_HOST>
dig +short <PROCESSING_PUBLIC_HOST>
```
Ответ должен совпасть с `<VM_PUBLIC_IP>`.
@@ -143,8 +173,9 @@ sudo DEPLOY_USER=deploy \
- проверить его версию/review;
- не передавать реальные IP/ключи в git;
- проверить auto reboot unattended upgrades относительно maintenance;
- решить, действительно ли deploy user нужен в группе `docker` (это root-equivalent);
- не применять `AllowTcpForwarding no`, если утверждённый break-glass DB tunnel необходим; предпочтителен VPN/bastion.
- гарантировать, что `deploy` не добавлен в группу `docker` (это root-equivalent);
- оставить `AllowTcpForwarding no` по умолчанию; если нужен DB tunnel, создать отдельного `tunnel` с `AllowTcpForwarding local`, конкретным `PermitOpen`, без sudo/TTY/agent/X11 forwarding;
- установить root-owned systemd-units и `/etc/sudoers.d/deploy` с полными командами и конкретными unit names без wildcard.
### 5.2. Проверки
@@ -160,12 +191,14 @@ df -h
free -h
```
Открыть второй SSH session как `deploy`, затем отключить root/password login. `.env` позже имеет mode `0600`.
Открыть второй SSH session как `deploy`, затем отключить root/password login. Production `.env` позже содержит только non-secret config; runtime secrets доставляются по arch-06.
### Gate 2
- [ ] SSH key login `deploy` проверен во втором сеансе.
- [ ] Root/password auth выключены.
- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды.
- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись.
- [ ] UFW и DOCKER-USER активны после restart Docker.
- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией.
- [ ] NTP active; disk/swap соответствуют sizing.
@@ -173,6 +206,32 @@ free -h
**Ожидаемый результат:** reboot VM не теряет SSH, firewall и Docker service.
### 5.3. Дополнительный lockdown private/no-egress VM
Для SigNoz и другой VM, которая после раскатки не должна иметь internet ingress/egress, bootstrap выполняется по lifecycle arch-06.
До закрытия временного доступа:
- [ ] SSH разрешён только из trusted ops CIDR и только по ключам.
- [ ] Пакеты/images получены из утверждённых источников, версии/digests зафиксированы.
- [ ] Health/readiness успешны.
- [ ] Проверены необходимые private flows (например app-VM → OTLP/SigNoz).
- [ ] Проверен private путь администрирования через bastion/VPN.
- [ ] Секреты размещены root-owned файлами; временные копии удалены.
Lockdown:
- [ ] Public IP удалён, если поддерживается и не нужен.
- [ ] Public SSH и любой internet ingress удалены из cloud SG и host firewall.
- [ ] Общий internet egress закрыт; оставлены только явно утверждённые private flows.
- [ ] С внешней машины SSH и service ports недоступны.
- [ ] С VM не проходит неразрешённый internet egress.
- [ ] Из private network работают SSH и обязательные service flows.
- [ ] Временные bootstrap credentials/rules/files удалены.
- [ ] Результат и время закрытия записаны в release checklist.
Повторное открытие ingress/egress выполняется только как ограниченная по CIDR/destination и времени break-glass операция. После неё весь lockdown checklist повторяется.
## 6. Stage 3 — managed PostgreSQL
### 6.1. Защита и восстановление managed PostgreSQL
@@ -188,7 +247,7 @@ free -h
CA managed PostgreSQL скачать из панели или документации провайдера в защищённый путь VM, например `/opt/han-chat/secrets/pg/ca.pem`, с владельцем `root:deploy` и mode `0440` (либо `0400`, если файл читает один пользователь). Все DSN используют `sslmode=verify-full` и `sslrootcert=/run/secrets/pg-ca.pem` либо эквивалент драйвера. Режимы `disable`, `allow`, `prefer` и `require` без проверки CA для production запрещены.
Публичный HTTPS-сертификат `<PUBLIC_HOST>` выпускается отдельно через **Let's Encrypt** на Stage 9. Он устанавливается в корневой nginx, а не в PostgreSQL.
Публичные HTTPS-сертификаты `<PUBLIC_HOST>` и `<PROCESSING_PUBLIC_HOST>` выпускаются отдельно через **Let's Encrypt** на Stage 9 и устанавливаются только в nginx соответствующей VM, а не в PostgreSQL.
### 6.2. Роли
@@ -207,7 +266,7 @@ CA managed PostgreSQL скачать из панели или документа
5. runtime: `USAGE`, DML и sequence grants только на свои objects;
6. `ALTER DEFAULT PRIVILEGES` от migration owner;
7. запретить чужие schemas и public schema create;
8. `bitrix_sync_user` не получает `han_app` grants, пока module-07 остаётся stub.
8. `bitrix_sync_user` получает только column/table grants и approved procedures из module-07 §13 после применения полной sync migration; broad schema write запрещён.
Команда bootstrap требует `<PROJECT_PATH>`:
@@ -241,7 +300,7 @@ psql "host=<PG_PRIVATE_HOST> port=<PG_PORT> dbname=<PG_DATABASE> user=<RUNTIME_U
1. `api-backend` Alembic владеет `han_app`, triggers, seed;
2. `bitrix-local-app` Alembic владеет `bitrix_local`;
3. `message-safety` stub не создаёт PG tables до production implementation;
4. `bitrix-sync` stub — optional empty baseline;
4. `bitrix-sync` migration role владеет schema `bitrix_sync`; api-backend Alembic отдельно мигрирует shared `han_app.sync_queue`, triggers и mapping;
5. Keycloak мигрирует standard tables сам; custom provider имеет собственные versioned migrations;
6. `sms-service` владеет versioned migrations/seed schema `sms`; runtime `sms_user` не имеет доступа к `han_app`/`keycloak`.
@@ -282,18 +341,18 @@ CORS quarantine:
{
"AllowedOrigins": ["https://<PUBLIC_HOST>"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type", "x-amz-*"],
"AllowedHeaders": ["Content-Type", "If-None-Match", "x-amz-checksum-sha256", "x-amz-*"],
"ExposeHeaders": ["ETag", "x-amz-checksum-sha256"],
"MaxAgeSeconds": 600
}
]
```
Уточнить фактические required signed headers. Не разрешать `*` origin с credentials.
Required signed headers для target flow: `Content-Type`, `If-None-Match: *`, checksum. Не разрешать `*` origin с credentials.
Lifecycle:
- quarantine: expire orphan objects только после периода, превышающего Safety poll + recovery; initial 2 дня, согласовать;
- quarantine: failed/orphan expire через 48 ч и только при отсутствии active `safety_tasks`;
- incomplete multipart upload: abort через 1 день;
- attachments/documents: без auto-delete до legal retention;
- noncurrent versions: policy после legal review.
@@ -303,7 +362,10 @@ Lifecycle:
- [ ] Все buckets private.
- [ ] API key не может list/write вне exact scope.
- [ ] Safety key не может write/delete.
- [ ] Browser test origin выполняет presigned PUT.
- [ ] Browser test origin выполняет presigned PUT с checksum и `If-None-Match: *`; повтор того же key получает `412`.
- [ ] Complete фиксирует authoritative `version_id`, ETag и checksum; Safety читает только эту version.
- [ ] Wrong version/ETag и изменённый source дают deny/error и не promote-ятся.
- [ ] Conditional promote mismatch не создаёт delivery outbox/Bitrix call.
- [ ] Quarantine lifecycle не удалит active `safety_tasks`.
- [ ] Data lifecycle соответствует retention.
@@ -483,7 +545,11 @@ cd <BACKEND_ROOT>
docker compose config --services
```
В целевом real-SMS release ожидаются: `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`, `sms-worker` (либо документированный worker process), `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector` и one-shot jobs/profile components.
В target release:
- root Compose ВМ1: edge `nginx`, `api-backend`, `keycloak`, `sms-service`, `sms-worker`, `bitrix-local-app`, Redis DB0/DB1, local `otel-collector`;
- root Compose ВМ2: собственный public/private nginx, `message-safety-api`, `message-safety-worker`, `clamd`, `freshclam`, `bitrix-sync`, Redis Safety, local `otel-collector`;
- API и worker Safety используют один immutable image, но отдельные processes/containers. MVP: 1 API + 1 worker container с 5 file-worker slots; при провале gates сначала увеличиваются slots/worker replicas по queue depth.
Networks:
@@ -520,7 +586,7 @@ Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config rea
## 12. Stage 9 — TLS bootstrap, фаза 1
Прототип `ssl-issue.sh` останавливает весь Compose и использует standalone Certbot. Для full stack предпочтителен **webroot two-phase**, чтобы не делать `compose down`.
Прототип `ssl-issue.sh` останавливает весь Compose и использует standalone Certbot. Для full stack предпочтителен **webroot two-phase**, чтобы не делать `compose down`. Процедура выполняется независимо в root Compose каждой VM: для ВМ1 с `<PUBLIC_HOST>`, для ВМ2 с `<PROCESSING_PUBLIC_HOST>`.
### Phase A: HTTP bootstrap
@@ -528,7 +594,7 @@ Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config rea
2. Запустить nginx с bootstrap config: только `/.well-known/acme-challenge/` и redirect; TLS block не требует отсутствующий cert.
3. Запустить Certbot profile:
Сначала проверить процесс через Let's Encrypt staging CA, добавив `--staging`. Staging-сертификат не является доверенным браузерами и нужен только для проверки DNS, firewall, ACME webroot и конфигурации. После успешной проверки удалить staging lineage либо выпустить production-сертификат с отдельным `--cert-name <PUBLIC_HOST>`.
Сначала проверить процесс через Let's Encrypt staging CA, добавив `--staging`. Staging-сертификат не является доверенным браузерами и нужен только для проверки DNS, firewall, ACME webroot и конфигурации. После успешной проверки удалить staging lineage либо выпустить production-сертификат с отдельным `--cert-name` текущего host. Ни сертификат, ни ACME volume между VM не разделяются.
Production-выпуск:
@@ -654,7 +720,7 @@ docker compose run --rm api-backend alembic upgrade head
docker compose run --rm bitrix-local-app alembic upgrade head
```
Для message-safety stub PG migration отсутствует. Для bitrix-sync stub — baseline только если реализован. Keycloak стандартную schema мигрирует выбранная pinned версия при controlled startup; provider migration выполняется отдельным approved job.
Для message-safety stub PG migration отсутствует. Перед full sync cutover сначала применяется backward-compatible api-backend migration shared queue/mapping/trigger, затем migration schema `bitrix_sync`; grants выдаются после обеих migrations и negative permission test. Keycloak стандартную schema мигрирует выбранная pinned версия при controlled startup; provider migration выполняется отдельным approved job.
### 13.3. Seed `app_settings`
@@ -698,6 +764,35 @@ Production cutover запрещён при любом placeholder, несогл
Rollback SMS: немедленно вернуть Keycloak в mock mode; не удалять schema/journal и не откатывать migrations без доказанной backward compatibility. Остановить новые real orders, дать worker завершить либо зафиксировать in-flight/`uncertain`; предпочтителен forward-fix.
### 13.5. Controlled rollout `bitrix-sync`
До включения `BITRIX_SYNC_ENABLED=true`:
1. создать/проверить custom Contact fields `user_id`, registration flag, citizenship и занести их non-secret names в env; для universal CRM имя `UF_CRM_<digits>` преобразуется в `ufCrm_<digits>`, active filter использует `1`, а wire-значения add/update подтверждаются contract test;
2. создать smart process «Конфликты синхронизации», поля/стадии/ответственного/SLA и активировать validated `bitrix_sync.settings`;
3. завести отдельный входящий webhook технического пользователя с минимальными правами module-07 §13;
4. настроить два HTTP-webhook робота на Contact/alert receiver URLs `https://<PROCESSING_PUBLIC_HOST>/bitrix/sync/webhook/...?token=<receiver-token>`; использовать отдельные высокоэнтропийные query tokens из штатного secret manager, исключить query/body из журналов; local app/event handler для CRM sync не создавать;
5. применить expand migrations `han_app`, затем `bitrix_sync`, после чего выдать точечные GRANT и выполнить negative permission tests;
6. зафиксировать `cutover_watermark` и one-shot операцией отменить существующие до него pending/retry contact-задачи с причиной `initial_full_sync_cutover`;
7. не создавать backfill: это утверждённое ограничение первого релиза;
8. запустить image с sync disabled, проверить `/health/live`, expected `sync_disabled`, secret/config validation, portal host/member ID и smoke разрешённых Bitrix methods, включая `crm.item.list` для `entityTypeId=3` с `>=updatedTime`, `opened=1` и registration field `=1`;
9. открыть на nginx ВМ2 только два exact webhook routes для version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`, выполнить nginx config test, valid/invalid source IP и query-token form-urlencoded contract tests, подтвердить отсутствие query/body в logs/traces и отсутствие запросов на ВМ1;
10. включить sync, проверить обработку только post-watermark canary user, mapping в `bitrix_sync`, отсутствие CRM ID в App DB, suppression, alert/rebind и telemetry;
11. наблюдать не менее agreed canary window queue age, CRM limit errors, DLQ, webhook/reconciliation lag, число Contact, восстановленных reconciliation без webhook, source-IP rejects и SigNoz alerts; scheduled full reconciliation не запускать.
Rollback: закрыть public webhook routes на nginx ВМ2 либо вернуть retryable `503`, остановить claims, bounded drain in-flight, установить `BITRIX_SYNC_ENABLED=false`. ВМ1 не изменяется. Уже созданные Contact/mapping автоматически не удалять; pending после watermark не отменять; schema downgrade с production rows не выполнять.
#### Изменение source IP Битрикс24
Сигнал для проверки allow-list — всплеск Contact, восстановленных инкрементальной reconciliation без обработанного webhook, особенно одновременно с ростом `webhook_rejected_total{reason="source_ip"}`.
1. Сопоставить окно всплеска с bounded-retention журналом source-IP rejects nginx ВМ2; query и body не извлекать и не сохранять.
2. Подтвердить принадлежность нового адреса инфраструктуре Битрикс24/портала по согласованному каналу или контролируемым probe. Наличие корректного query token само по себе не является подтверждением.
3. Добавить минимально необходимый IP/CIDR в version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`, выполнить peer review, manifest validation и `nginx -t` через штатный deployment unit.
4. Применить safe reload, проверить приём Contact/alert webhook и отсутствие query/body в logs/traces.
5. Убедиться, что source-IP rejects прекратились, webhook lag нормализовался, а следующие инкрементальные reconciliation run не показывают растущих восстановлений.
6. При ошибочном расширении немедленно вернуть предыдущую approved версию allow-list. Автоматическое добавление наблюдаемого IP запрещено.
## 14. Stage 11 — Keycloak bootstrap
### 14.1. Первый старт
@@ -749,33 +844,13 @@ Custom OTP tables мигрируются versioned mechanism до включен
Архитектурный порядок:
1. Redis;
2. OTEL Collector;
3. API backend/settings;
4. SMS service/worker после migrations (при SMS release; Keycloak пока mock);
5. Keycloak;
6. Message Safety;
7. Bitrix local app;
8. Bitrix sync;
9. nginx.
1. На ВМ2 approved systemd unit поднимает Redis Safety и local Collector.
2. Затем `clamd`/`freshclam`, Safety API/worker и `bitrix-sync`.
3. Последним на ВМ2 поднимается nginx с независимыми public `80/443` и private `8443` server blocks; проверяются оба TLS-контура, exact webhook routes, capability health, signature age и отрицательные ingress/egress tests.
4. На ВМ1 unit поднимает локальные Redis/Collector, API, SMS, Keycloak, local app и edge nginx.
5. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 разворачиваются независимо и не требуют изменения ВМ1.
Команды:
```bash
cd <BACKEND_ROOT>
docker compose up -d redis
docker compose up -d otel-collector
docker compose up -d api-backend
docker compose up -d sms-service sms-worker
docker compose up -d keycloak
docker compose up -d message-safety
docker compose up -d bitrix-local-app bitrix-sync
docker compose up -d nginx
docker compose up -d --wait api-backend keycloak sms-service bitrix-local-app
docker compose exec -T nginx nginx -t -c /tmp/nginx.conf
docker compose kill -s HUP nginx
docker compose ps
```
Legacy single-VM `docker compose up` из старого stub-контура не является evidence готовности target ВМ2.
При повторной раскатке reload после readiness upstream обязателен: nginx
разрешает Docker DNS при загрузке конфигурации и иначе может продолжить
@@ -796,17 +871,17 @@ Expected:
- Redis `PONG`;
- Keycloak DB/realm/provider ready;
- Collector health + exporter queue;
- Safety ready и Redis DB2;
- Safety v2 capability ready и Redis Safety (target); legacy DB2 проверяется только в stub acceptance;
- API DB/Redis/JWKS/settings/S3/Safety ready;
- local app до Bitrix install может быть `portal_not_installed`;
- bitrix-sync возвращает `mode=db_connectivity_stub`, не CRM-ready;
- bitrix-sync до enablement возвращает `sync_disabled`; после preflight — `mode=full`, validated settings/secrets/grants, живые worker/limiter и актуальные reconciliation cursors;
- nginx config test success.
### Gate 12
- [ ] Все containers live, нет restart loop/OOM.
- [ ] Critical readiness green.
- [ ] Expected degraded statuses только Bitrix not-installed/sync stub.
- [ ] Expected degraded statuses только документированные; sync stub mode отсутствует.
- [ ] `docker compose ps` не публикует internal ports.
- [ ] Internal `/internal/*` снаружи 404.
- [ ] OTEL принимает telemetry.
@@ -853,18 +928,22 @@ docker compose run --rm --no-deps <TOOLBOX_SERVICE> \
## 17. Stage 14 — public smoke и E2E
### 17.1. Edge
### 17.1. Независимые public ingress
```bash
curl -I http://<PUBLIC_HOST>/
curl -fsS https://<PUBLIC_HOST>/api/v1/public/app-config
curl -fsS https://<PUBLIC_HOST>/api/v1/public/content
curl -fsS https://<PUBLIC_HOST>/auth/realms/han-chat/.well-known/openid-configuration
curl -i https://<PUBLIC_HOST>/internal/safety/v1/messages/check
curl -i https://<PUBLIC_HOST>/internal/safety/v2/messages/check
openssl s_client -connect <PUBLIC_HOST>:443 -servername <PUBLIC_HOST>
curl -I http://<PROCESSING_PUBLIC_HOST>/
curl -i https://<PROCESSING_PUBLIC_HOST>/internal/sync/v1/status
curl -i https://<PROCESSING_PUBLIC_HOST>/bitrix/sync/webhook/contact
openssl s_client -connect <PROCESSING_PUBLIC_HOST>:443 -servername <PROCESSING_PUBLIC_HOST>
```
Expected: 308; public 200 strict DTO; discovery 200; internal 404; valid cert.
Expected ВМ1: 308; public 200 strict DTO; discovery 200; internal 404; valid cert. Expected ВМ2: HTTP redirect/ACME policy, internal 404, GET webhook 405/404, valid отдельный cert. Valid/invalid POST webhook проверяется отдельным form-urlencoded contract test с разрешённого и запрещённого source IP без помещения query token в shell history или логи.
### 17.2. Auth/frontend
@@ -883,7 +962,7 @@ Expected: 308; public 200 strict DTO; discovery 200; internal 404; valid cert.
Обязательные E2E:
- text, начинающийся после normalization с `ф/Ф` → public `422 message_blocked`, Bitrix не вызван;
- text с цифры → Safety `203`, API poll до `200` или test terminal `400`; клиент никогда не получает `203`;
- legacy stub-only: text с цифры → v1 `203`/test terminal `400`; target v2 smoke использует `202`/`200|403|terminal 503`, клиент internal pending не получает;
- прочий text → allow;
- terminal stub `400` преобразуется в `422`, не в generic validation;
- timeout → `503/504`, checkpoint/recovery, без duplicate;
@@ -1196,8 +1275,11 @@ certbot delete active cert
- managed PG private/TLS/backups/least privilege/migrations работают;
- S3 private/IAM/CORS/lifecycle проверены;
- exact release/images/frontend deployed;
- `.env` validated, secrets protected;
- один root Compose, один nginx, только 80/443;
- non-secret `.env` validated; runtime secrets разделены по сервисам и защищены;
- `deploy` не имеет Docker/root-equivalent доступа; production files root-owned, sudo ограничен конкретными systemd-units;
- один root Compose и nginx на VM; ВМ1 и ВМ2 имеют независимые public 80/443, ВМ2 дополнительно private 8443; public ВМ2 ограничен exact CRM webhook;
- container hardening и сетевые границы соответствуют arch-06;
- для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress;
- Redis/Collector volumes/resources/security работают;
- Keycloak realm/provider/PKCE/OTP готов;
- ordered startup/readiness пройден;
@@ -1208,11 +1290,43 @@ certbot delete active cert
- ops/incident/upgrade/DR owners назначены;
- все assumptions/TBD приняты до открытия traffic.
### VM2 cutover, rollback, reprovision и Freshclam
Cutover gates: private TLS chain/SAN; Safety v2 PG migration и lease/fencing smoke; capability `text|links|files|worker`; S3 Gate 4; performance acceptance; egress negative tests. Safety Service Owner, Rule Pack Owner, Security Owner, Product Owner и Operations Owner фиксируют approvals. Только после них ВМ1 переключает `MESSAGE_SAFETY_URL`. Legacy v1 остаётся rollback target на ограниченное окно, но один `message_id` нельзя одновременно отправлять в v1 и v2.
Rollback возвращает caller adapter/upstream ВМ1 на предыдущий immutable release. Уже созданные v2 tasks завершаются/reconcile по PG checkpoints; down-migration и удаление quarantine versions запрещены.
При потере ВМ2 fail-open запрещён. ВМ2 reprovision-ится из immutable image/config; secrets materialize под отдельным IAM, Redis поднимается пустым, migrations/capability/egress gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год.
#### Message Safety config activation
Первая migration создаёт `message_safety.config_versions` и seed version 1; readiness не открывается без ровно одной valid active version. Config-only rollout выполняется отдельным root-owned migration/config job под config-admin DB role: создать immutable draft, проверить JSON Schema/cross-field constraints и наличие rules/detector artifacts, записать approvals, затем транзакционно activate. Runtime API/worker имеют только `SELECT` к config table.
После activation operator проверяет health `config_version`, text/link/file canaries, cache-key version и отсутствие изменения in-flight task version. Изменение, ослабляющее policy или увеличивающее limits, требует Security Owner; остальные — Safety Service Owner и Operations Owner. Rollback не реактивирует retired row: предыдущий payload клонируется в новую monotonic version и активируется с отдельным audit event.
#### Emergency MOCK
`deploy` может без root login включить/изменить/выключить mode:
```bash
sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free true
sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free false
sudo /usr/local/sbin/han-message-safety-mode standard
```
Root устанавливает helper и `/etc/sudoers.d/deploy-message-safety-mode` при bootstrap. Sudoers разрешает `deploy` только этот immutable `root:root 0755` executable; helper имеет строгий parser без shell eval/path arguments, атомарно обновляет `root:han-message-safety 0640` `/etc/han-chat/message-safety-mode.env` (dedicated GID `10001` доступен только non-root контейнеру), валидирует Compose config, выполняет фиксированную Message Safety API recreate/restart operation внутри root project и проверяет private health. На ошибке он восстанавливает предыдущий mode/config и повторяет restart. `deploy` не получает write к config, systemd units, Compose и Docker socket.
Перед включением operator фиксирует incident/change ID и выбранные text/file policies; после команды проверяет `processing_mode=mock`, forced canaries, отсутствие `202`, metric/active alert и audit actor. Ранее принятые standard tasks сохраняют mode и завершаются без переклассификации; только новые requests используют MOCK. Автоматического timeout нет: mode действует без ограничения по времени до явного `standard`. Поэтому перед закрытием incident обязателен возврат в standard, проверка normal capabilities и text/link/EICAR canary. File, разрешённый в MOCK, маркируется `scan_status=bypassed`, а не `clean`.
`freshclam` имеет controlled egress только к утверждённому signature CDN. Max signature age 24 ч; stale/failed update выключает только `files` и поднимает alert. Новая база проходит integrity/load/EICAR canary и atomic activate/reload; при regression возвращается последняя валидная база.
Initial ВМ2: 4 vCPU/8 GiB/80 GiB, resource/PID limits и backpressure. Workers масштабируются первыми по queue depth, `clamd` — scan lanes. ВМ3/scale-out инициируются при sustained CPU/RAM >70%, queue age >30 с, провале module-05 performance gates, contention `bitrix-sync` или независимом release cadence.
## 28. Допущения, TBD и архитектурные конфликты
### Допущения
- D-A1: одна VM и один public host на MVP.
- D-A1: отдельные public hosts ВМ1/ВМ2; processing host публикует только CRM webhook, остальные сервисы ВМ2 остаются private.
- D-A2: обязательный минимум — `otel-collector`; доступность remote backend не предполагается до закрытия D-TBD11, local Grafana stack не обязателен.
- D-A3: managed provider даёт private network, TLS, backups/PITR.
- D-A4: Bitrix portal/connector/line остаются разрешёнными значениями architecture.
@@ -1221,25 +1335,25 @@ certbot delete active cert
### TBD до production
- D-TBD1: реальные domains, Expo native redirect URI и Bitrix placement frame ancestors.
- D-TBD2: final VM/PG sizing, RPS/WS, SLO/RPO/RTO.
- D-TBD2: final VM1/PG sizing, public API/WS SLO и общие RPO/RTO; Safety load/latency gates уже зафиксированы, monthly availability SLO Safety в MVP намеренно не вводится.
- D-TBD3: legal retention/erasure для PG/S3/audit/telemetry.
- D-TBD4: secret manager и rotation windows.
- D-TBD5: production Safety вместо stub и antivirus inbound operator files.
- D-TBD6: полноценный bitrix-sync/GRANT или явное исключение CRM sync из release.
- D-TBD5: реализовать/cutover production Safety v2; inbound operator files сознательно не проходят AV в MVP.
- D-TBD6: реализовать код, migrations, portal fields/smart process/webhooks и выполнить bitrix-sync cutover по module-07.
- D-TBD7: pinned Keycloak/nginx/Collector versions и SPI compatibility.
- D-TBD8: exact migration/seed/toolbox CLI commands после реализации repo.
- D-TBD9: Keycloak admin VPN/MFA topology.
- D-TBD10: final CSP/CORS/S3 headers и cloud-specific IAM.
- D-TBD11: выбрать observability backend/provider, endpoint/auth/retention и alert route либо явно ограничить среду acceptance-режимом без production-ready SLO.
- D-TBD11: уточнить auth/retention/alert route выбранного private SigNoz; backend и endpoint уже зафиксированы.
### Обнаруженные конфликты
1. `arch-01/02/03` описывают полноценный `bitrix-sync`, но module-07 реализует только `SELECT 1`. Первый release не поддерживает обещанную CRM profile sync.
2. `arch-01/02` ожидают production-like Safety и S3 scan, но module-05 — Redis-only random stub, file-only default allow и test-only terminal `400`. Это блокер настоящего production, даже если допустимо для production-like acceptance.
1. `module-07` теперь задаёт полный target `bitrix-sync`; до реализации кода/migrations/portal prerequisites сервис обязан оставаться disabled, документация сама по себе не означает выполненный cutover.
2. Текущая implementation остаётся v1 stub; module-05 описывает draft target v2. Cutover является production gate.
3. Прототипный PG init даёт runtime role `CREATE` schema и не разделяет migration/runtime roles; runbook требует ужесточения.
4. Прототипные TLS scripts используют отдельный service Compose/standalone downtime, тогда как целевая архитектура требует root Compose и two-phase webroot.
5. Prototype публиковал `/bitrix-internal/*` и использовал `/internal/v1/*`; целевой контур это запрещает и использует `/internal/openlines/v1/*`.
6. `arch-04` не содержит ряд proposed env из module-0409; production `.env.example` должен быть синхронизирован до реализации.
6. Runtime artifacts `.env.example`/Compose ещё могут не содержать зафиксированные VM2 env; документация не означает выполненный cutover.
7. Точные RPO/RTO, SLO, Keycloak version и Bitrix retry semantics не утверждены; OTP TTL задаётся `app_settings`, SMS journal по module-11 хранится бессрочно.
8. `init-managed-postgres.py` по умолчанию не задаёт TLS parameters при bootstrap connection и печатает credential-bearing DSN; его production-hardening обязателен.
9. Текущие Compose/env/config artifacts могут ещё не содержать `sms-service`; документация не разрешает real mode до реализации и прохождения rollout gates.
+140
View File
@@ -0,0 +1,140 @@
> Статус: исходный концепт и история обсуждения. Каноническая реализационная постановка после принятия решений — [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md). При расхождении применяется module-07.
>
> Актуализация 2026-08-06: production-проверка reconciliation использует `crm.item.list`, `entityTypeId=3`, фильтры `>=updatedTime`, `opened=1`, `ufCrm_1778692456=1`. Указанные ниже ранние варианты `UF_CRM_6A70C275346A7`, `Y/N` и поиск по двум телефонным маскам сохранены только как история и не являются реализационным контрактом.
MCP Server Bitrix24 с документацией https://mcp-dev.bitrix24.tech/mcp
### 1. Границы релиза
1.1. Что входит в **первый** релиз sync:
`contact.map_or_create`, `contact.update`,
передача в Битрикс24 тэга о том, что пользователь зарегистрирован в приложении
webhook Bitrix→App при изменении данных у пользователей, зарегистрированных в приложении
обновление сведений о данных пользователя при изменении их в Битрик24,
ведение бизнес-логов с конфликтами и ошибкам, требующих внимания.
создание и отслеживание статуса alerts по конфликтам
1.2. Что явно **вне scope**:
Lead/Deal,
документы компании в профиль,
merge контактов,
ручной replay API
1.3. Можно ли выпускать без двусторонности (только App→Bitrix), или webhook обязателен сразу?
вебхук обязателен в первой сборке
### 2. Сущности и маппинг полей
2.1 Какие поля `ClientProfile` / `UserIdentity` синхронизируем (в скобках поле в битрикс24):
`full_name` ↔ name
`citizenship` ↔ ufCrm_1768493029,
`russian_phone` ↔ phone,
`email` ↔ email
Значение citizenship возвращается ИД. Справочник Битрикс можно загрузить
curl -sS -G 'https://<portal-name>/rest/<user-id>/<token>/crm.contact.userfield.list' \
--data-urlencode 'filter[FIELD_NAME]=uf_crm_1768493029'
При запросах важно соблюдать следующий принцип: сервис проектируется таким образом, чтобы запрашивать минимум необходимой информации.
Т.е.
а) если мы синхронизинуем 5 полей, то мы запрашиваем ровно 5 нужных полей. Не запрашиваем все данные по клиенту.
б) если нам надо синхронизировать контакт по телефону - мы ищем в Б24 контакт только по этому телефону. Не запрашиваем список контактов.
Второй момент, выявленный в ходе тестирования:
Номер телефона может храниться в двух форматах - "+7xxxxxxxxxx", "+7 (xxx) xxx-xxxx"
Соответственно, по каждому искомому телефону делаем запрос по двум маскам, указанным выше.
Также сервис следует проектировать с учетом ограничений Битрикс24 по кол-ву обращений к АПИ (генерируем очередь и делаем запросы по расписанию через batch).
Пример запроса требуемых данных для одного пользователя:
curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"entityTypeId":3,"select":["id","name","phone","ufCrm_1768493029","createdTime"],"filter":{"@phone":["+7xxxxxxxxxx","+7 (xxx) xxx-xxxx"],"opened":"Y"}}' \
https://<portal-name>/rest/<user-id>/<token>/crm.item.list
2.2. Нужен ли флаг «контакт зарегистрирован в приложении» в Bitrix, и в каком поле?
Нужен (поле UF_CRM_6A70C275346A7: Y/N).
### 3. Правила матчинга Contact
3.1. Ключ поиска: только телефон (как выше указывал телефон хранится по одной из двух масок; возможно в документации есть более стабильные методы поиска контакта по номеру телефона)
3.2. Управление конфликтами:
Для разбора конфликтов должны создаваться alert: задача в Б24 (смарт процесс "Конфликты синхронизации") и бизнес-лог в БД с типом проблемы, деталями, ссылкой на ИД задачи в Б24 и ее текущим статусом.
Жизненный цикл alert: alert создается СС, обработка alert осуществляется в Б24 через смарт процесс "Конфликты синхронизации", БД регулярно опрашивает статус задач. После успешного разбора конфликта, СС корректирует статус в БД на "Завершено". ИД alerts нумеруются сиквенсом и передаются в Б24 при постановке задачи.
Описанный механизм разбора конфликтов - используется для разбора бизнес-расхождений. Он не используется в случае технических сбоев или ошибок приложения.
3.3. Жизненный цикл пользователя приложения в контексте связи с контактом (на примере 1 пользователя):
а) Клиент зарегистрировался по номеру телефона.
СС запрашивает в Б24, есть ли клиенты с указанным номером телефона.
Варианты:
В0. 0 контактов. СС должен инициировать create, получить информацию об ИД нового пользователя, записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению.
В1.1. 1 контакт + контакт не имеет связи с приложением. СС должен записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Если при этом в приложении флаг уже был, нам не важно.
В1.2. 1 контакт + контакт уже привязан к другому активному пользователю приложения. СС должен инициировать create, получить информацию об ИД нового пользователя, записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Плюс СС создает alert об ошибке привязки.
В2.1. 2 и более. СС выбирает самый новый контакт по creationtime. Выбранный контакт не имеет связи с приложением. СС должен записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Плюс СС создает alert наличии дублей контактов: необходимо проверить актуальность контактов, корректность закрепления.
В2.2. 2 и более. СС выбирает самый новый контакт по creationtime. Выбранный контакт уже привязан к другому активному пользователю приложения. СС должен инициировать create, получить информацию об ИД нового пользователя, записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Плюс СС создает alert об ошибке привязки: с указанием деталей события (в том числе информация о наличии нескольких контактов).
б) Процесс обновления данных.
Сейчас реализовываем только движение Б24 -> Приложение.
Б24 отслеживает изменения данных контактов, у которых есть признак UF_CRM_6A70C275346A7 = 'Y'
При изменении Б24 направляет вебхук СС
СС при получении вебхука складывает в очередь задание на обновление данных соответствующего контакта.
По расписанию выполняется запрос данных контактов из Б24. И обновляются сведения. Если в ответ на запрос по контакту не найдены данные, формируется alert (Пользователь приложения привязан к несуществующему контакту).
в) Клиент удалил профиль из приложения.
Обновляем признак UF_CRM_6A70C275346A7 = 'N'. Деактивируем учетную запись.
Механизм изменения привязки ИД Битрикс24 к пользователю приложения - администратор вносит изменения напрямую в БД
### 4. Очередь и worker
4.1. Этот пункт - примерное видение. По нему можно смело предлагать улучшения, тк. получается несколько связанных асинхронных процессов.
В схеме han_app хранятся бизнесовые задачи на обновление данных. Должен быть воркер, который забирает эти задачи и стартует требуемые сценарии (map_or_create, update, delete, alert_create, alert_status_update).
Каждый сценарий предполагает набор действий и запросов в Б24. Обработчик сценария в рамках его исполнения формирует задачи на обмен данными с Б24 (один сценарий может предолагать несколько связанных задач). В следующем пункте расписал примерные запросы в Б24, которые могут возникать в ходе сценария. Тебе нужно более подробно сформулировать сценарий и действия.
Задачи на обмен с Б24 складываются в соответствующую таблицу в схеме sync. Обмен данными с Б24 осуществляется через механизм Батчей. Кроме операций синхронизации документов (не входят в текущую реализацию). Один вложенный в батч запрос должен относиться к одной задаче синхронизации.
Отправка запроса осуществляется по шедулеру (по умолчанию каждые 5 секунд). Для этого отдельный воркер собирает задачи, находящиеся в статусе pending + кол-во tryes <max_retries (не более 20 штук в один батч). Задачи отбираются по принципу fifo. Если задач меньше 20, то батч формируется из меньшего числа задач. Если задач 0, то батч не формируется, запрос в битрикс не отправляется.
Отобранные задачи переводятся в следующий статус (модель статусов предложи сам). После получения ответа успешные ответы переходят на следующий статус, неуспешные ответы, требующие retry, возвращаются в статус pending, кол-во tries + 1. Успешный ответ, касающийся задачи, сохраняем в таблице с задачами.
Триггер (или шедулер) проверяет наличие задач в статусе pending + кол-во tries >=max_retries. Если находит, переводит в статус fail + отбрасывает бизнес-лог для разбора (в Битрикс24 задача не создается).
Значения параметров прописываются в настройках сервиса в БД в схеме sync_service, изменения параметров в БД должны применяться сервисом без перезагрузки.
Какие у меня архитектурные сложности возникли, ограничивающие возможность полноценно поставить требования:
1) Битрикс24 допускает до 5 api запросов в секунду. По идее при высокой нагрузке это может означать что регулярность шедулера можно настроить до 0,2 секунд. Я не понимаю, нужно ли тут какой-то параллелиризм вводить? или БД с одним шедулером справится, тк Б24 достаточно быстро отвечает. Но что произойдет, если Б24 за 0.2 секунды не ответит? Тут нужна твоя экспертиза и варианты.
2) Как правильно организовать работу сценариев. Сценарий может содержать несколько последовательных действий, требующих обмена с Б24 и не требующих. Жизненный цикл отработки сценария начинается с момента, как я забрал задачу из han_app, или ее поставил сам СС. Держать в оперативной памяти сценарий на протяжении его жизненного цикла неправильно, тк очередь задач может забиться и либо все рухнет, либо сценарии перестанут запускаться - формируется точка отказа. Правильнее сценарий разделить на условно-атомарные операции, и раскладывать их в таблицу. И тогда какие-то воркеры могут быстро бегать по этой таблице, находить очередную операцию в рамках сценария, которая ждет исполнения, исполнять ее, переводить в статус "исполнена", чтобы активировать к возможности исполнения следующую задачу. Мне этот вариант кажется более отказоустойчивым и управляемым (плюс в любой момент, даже если грохнется сервис, можно запуститься с того места, где он грохнулся, и никакие сценарии не оборвутся). Но я не до конца понимаю, как тут управлять производительностью (можно ли много воркеров запускать, чтобы они параллельно работали, и в какой момент это нужно делать).
4.2. Виды запросов в Б24 с привязкой к сценариям:
Сценарий регистрации пользователя:
1: поиск контакта по номеру телефона
2: создание нового контакта либо
3: обновление сведений контактов (изменений флага UF_CRM_6A70C275346A7)
Сценарий обновления
4: запрос данных по контакту
Сценарий удаления профиля:
3: обновление сведений контактов (изменений флага UF_CRM_6A70C275346A7)
Сценарий обработки alerts:
5: поставить задачу в Б24
6: узнать статус задачи в Б24
... возможно еще понадобятся.
Используемые виды запросов хранятся в справочнике видов запросов и могут использоваться сервисом синхронизации в рамках запущенных процессов.
4.3. Сервис синхронизации должен ставить задачи на синхронизацию с использованием утвержденных видов запросов в Б24.
4.4. Готовы ли триггеры App DB и GRANT для `bitrix_sync_user` , или это часть той же постановки?
Триггеры 'contact.map_or_create' и 'contact.update' при создании и изменении данных в таблицах пользователей готовы и работают. Действующие триггеры необходимо проверить, что они не будут пытаться синхронизировать изменения, полученные от битрикс24 и залитые в БД. Процессов постановки задач на обновление данных пользователя из-за вебхука от Б24, нет.
### 5. Bitrix24: доступ и webhook
5.1. Способ доступа к CRM REST: я верно понимаю, что local-app безопаснее входящего вебхука? Т.к невозможно, даже зная токен, отправить запрос в Б24? Если это так, то логично создать в Б24 еще одно локальное приложение и получать данные через него. Для вебхуков от Б24 делаем соответствующую ссылку с секретом.
5.2. Кто настраивает робота в Bitrix (какие события/поля). Сформируй требования к роботу и исходящему вебхуку, я настрою робот.
5.3. Есть ли тестовый портал отдельно от prod. Нет, портал один. При этом для разделения теста и прода будут использоваться различные local-app. Чтобы не смешивать базу, набор тестовых пользователей будет задаваться маской "нелегитимных" номеров.
### 6. Надёжность, безопасность, ops
6.1. Rate limit / квоты Bitrix REST: политика при `QUERY_LIMIT_EXCEEDED`?
Кол-во попыток определено в п.4.1. Если не удалось - отбрасываем бизнес-логи. Правило универсально как для ошибок синхронизации, так и для QUERY_LIMIT_EXCEEDED. Можешь предложить вариант лучше, если есть идеи.
6.2. PII в логах/метриках: секреты в логах не допускаются. PII в логах маскируются: два первых читаемых и два последних читаемых символа поля отображаем как есть, остальные символы маскируем. Ошибки логируем как есть - т.к. сервис внутренний и недоступен пользователям.
6.3. Cutover: stub → full sync на уже накопленной очереди — replay всех pending или только новых? Управляем через .env: replay = true, значит replay всех pending. replay = false, значит все pending при раскатке сервиса переводим в статус "Cancelled".