# module-06. Проектная спецификация `bitrix-local-app` > Статус: целевая production-спецификация MVP. > Портал: `han0107.bitrix24.ru`; connector: `han_mobile_app`; Open Line: `8`. > Источники: [`README.md`](README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md). ## 1. Назначение и приоритет Сервис является локальным серверным приложением Bitrix24 и адаптером Open Lines. Он изолирует OAuth и протокол `imconnector` от `api-backend`, надёжно доставляет разрешённые сообщения клиента оператору и события оператора обратно в HAN. При конфликте действуют приоритеты `README.md`. Настоящий документ детализирует существующие контракты, но не меняет их. Любой новый внешний/internal endpoint сначала фиксируется в `arch-02`. Канонический URL канала: ```text https://han0107.bitrix24.ru/contact_center/connector/?ID=han_mobile_app&LINE=8 ``` ## 2. Ответственность и границы Сервис отвечает за: - install/lifecycle локального приложения и OAuth Bitrix24; - шифрованное хранение и безопасное обновление portal tokens; - `imconnector.register`, `imconnector.activate`, `event.bind`, status/retry setup; - публичный приём `ONAPP*` и `ONIMCONNECTOR*`; - tolerant parsing JSON/form/multipart и PHP-style массивов; - проверку callback, нормализацию, durable inbox, retry и DLQ; - `dialog_sessions`: `external_chat_id` (= `dialog_id`) ↔ `bitrix_chat_id` ↔ `session_id`; - идемпотентный outbound `api-backend` → `imconnector.send.messages`; - forward входящих сообщений/файлов и `dialog.closed` в `api-backend`; - `imconnector.send.status.delivery` только после durable ack API; - health, telemetry, audit технических переходов. Сервис не отвечает за: - JWT/пользовательскую авторизацию, Message Safety и App DB; - хранение истории HAN, realtime и S3; - CRM Contact/profile sync — зона `bitrix-sync` на ВМ2, не этого модуля; - изменение `Dialog.status` в `han_app`; - публикацию internal API на edge. ## 3. Технологический профиль и структура - Python 3.12+, FastAPI, Pydantic v2, Uvicorn. - SQLAlchemy 2 async + `asyncpg`; Alembic. - Один долгоживущий `httpx.AsyncClient` с bounded pool. - PostgreSQL managed, только схема `bitrix_local`. - OpenTelemetry и JSON logging. ```text bitrix-local-app/ app/ main.py settings.py api/{public_bitrix,internal_openlines,health,schemas,errors,auth}.py application/{install,setup,outbound,inbound,forward,delivery_ack}.py domain/{entities,enums,policies}.py infrastructure/ bitrix/{client,oauth,connector,parser,normalizer}.py db/{models,repositories,uow}.py crypto/{token_cipher,keyring}.py resilience/{retry,circuit,rate_limit}.py observability/{logging,metrics,tracing}.py workers/{inbox_forward,outbox_delivery,setup_reconcile}.py alembic/ tests/{unit,integration,contract,e2e}/ openapi.yaml Dockerfile docker-compose.yml ``` Router только валидирует/аутентифицирует; use case задаёт транзакцию; Bitrix adapter скрывает внешний payload. ## 4. Публичные endpoint Сервис предоставляет следующие endpoint. Корневой nginx публикует первые три; health остаются internal по умолчанию и открываются exact-route только при явно выбранной ops/monitoring policy: | Method | Path | Назначение | |---|---|---| | GET/POST | `/bitrix/handler` | probe и callbacks `ONAPP*`/`ONIMCONNECTOR*` | | GET/POST | `/bitrix/install` | install callback/probe | | GET | `/bitrix/placement` | минимальный HTML placement | | GET | `/health/live` | liveness; internal по умолчанию | | GET | `/health/ready` | readiness; internal по умолчанию | `GET handler/install` возвращает безопасный `200`, не раскрывая OAuth/setup. POST принимает только bounded body и разрешённые content types. Placement имеет отдельный CSP `frame-ancestors` с точным allow-list Bitrix24. Internal `/internal/openlines/v1/*` доступны только по Docker/VPC network и **не маршрутизируются nginx наружу**. ## 5. Internal Open Lines API Все вызовы требуют: ```text Authorization: Bearer ${BITRIX_INTERNAL_API_TOKEN} X-Request-ID: UUID/ULID traceparent: optional W3C ``` Caller `api-backend` передаёт `BITRIX_LOCAL_APP_INTERNAL_TOKEN`, значение которого равно `BITRIX_INTERNAL_API_TOKEN`. Сравнение constant-time. ### 5.1. `POST /internal/openlines/v1/messages` Одна операция — одно сообщение MVP. `Idempotency-Key` обязателен и равен `message_id`. ```json { "message_id": "uuid", "external_chat_id": "uuid", "occurred_at": "2026-07-10T09:00:00Z", "user": { "id": "uuid", "display_name": "Новый клиент HAN", "phone": "+79990000000" }, "message": { "content_kind": "text", "text": "Здравствуйте", "files": [] } } ``` Файловый вариант: ```json { "message_id": "uuid", "external_chat_id": "uuid", "occurred_at": "2026-07-10T09:00:00Z", "user": {"id": "uuid", "display_name": "Новый клиент HAN", "phone": "+79990000000"}, "message": { "content_kind": "file", "text": "", "files": [{ "attachment_id": "uuid", "name": "document.pdf", "mime_type": "application/pdf", "size_bytes": 12345, "download_url": "https://short-lived-signed-url" }] } } ``` Правила: - `external_chat_id` строго UUID и равен App `dialog_id`; - `text` xor один file; unknown fields запрещены; - signed URL не сохраняется в обычные логи и редактируется в durable payload по истечении необходимости; - `display_name` — `ClientProfile.full_name`, если пусто — `Новый клиент HAN`; - телефон клиента передаётся в `user.phone` и мапится в `MESSAGES[0][user][phone]` для CRM-лида; email не передаётся; - fingerprint строится по стабильным полям без signed query; - тот же key/fingerprint возвращает прежний результат; - тот же key с иным fingerprint → `409 idempotency_key_reused`. Успех `200/201`: ```json { "status": "delivered", "message_id": "uuid", "external_chat_id": "uuid", "bitrix_message_id": "string-or-null", "dialog_session": { "bitrix_chat_id": 1807, "session_id": "sess-42" } } ``` `api-backend` выставляет `delivery_status=delivered` только после этого ответа/duplicate result. `202` не считается финальной доставкой в основном синхронном flow. ### 5.2. `GET /internal/openlines/v1/dialogs/{external_chat_id}` Возвращает active mapping: ```json { "external_chat_id": "uuid", "bitrix_chat_id": 1807, "session_id": "sess-42", "status": "open", "updated_at": "2026-07-10T09:00:00Z" } ``` `404` — mapping отсутствует/soft-deleted. Пользовательская PII не возвращается. ### 5.3. `GET /internal/openlines/v1/status` Возвращает безопасный статус portal OAuth, connector registration/activation, event bindings, worker backlog и circuit state; токены и raw Bitrix response исключены. `200` может иметь `status=degraded`; `503` — нет usable OAuth/БД. ### 5.4. `POST /internal/openlines/v1/setup/retry` Идемпотентно запускает reconcile `register → activate line 8 → event.bind`. Одновременно разрешён один run по portal advisory lock/DB lease. Ответ содержит per-step status. Endpoint ops-only с тем же Bearer и дополнительным service rate limit. ## 6. Outbound: HAN → Open Lines 1. Аутентифицировать caller и зарезервировать `outbound_messages` по `message_id`. 2. При completed вернуть сохранённый sanitized result. 3. Собрать `MESSAGES` Bitrix: `user.id/name/phone`, `message.id/date/text/files`, `chat.id`. 4. Вызвать `imconnector.send.messages` с `CONNECTOR=han_mobile_app`, `LINE=8`. 5. Извлечь `CHAT_ID`, session `ID`, Bitrix message id из допускаемых вариантов ответа. 6. В одной транзакции upsert `dialog_sessions`, записать result, status `delivered`. 7. Вернуть ack API. Ambiguous timeout не разрешает слепой повтор без idempotency/reconciliation. Worker сверяет локальный state/session и повторяет только если метод/Bitrix semantics не создадут дубль; иначе `manual_review`/DLQ. Automatic retry допустим для connect failure до отправки, explicit rate-limit и известных transient ошибок. ## 7. Install, OAuth и setup ### 7.1. Install POST install/handler с `ONAPPINSTALL`: 1. parse и strict validate `auth`; 2. проверить expected portal domain `han0107.bitrix24.ru`, HTTPS `client_endpoint`, `member_id`; 3. сохранить tokens до внешнего setup; 4. создать `install_runs`; 5. выполнить setup идемпотентно; 6. вернуть `installed` либо `installed_with_errors`; partial setup не теряет OAuth. `ONAPPUNINSTALL` помечает portal installation `uninstalled`, запрещает outbound и планирует revocation/retention. В отличие от прототипа, usable tokens не остаются active. ### 7.2. Token storage и encryption - `access_token`, `refresh_token`, `application_token` шифруются application-level envelope encryption (AES-256-GCM или эквивалент AEAD). - Master key только secret env/mount: `BITRIX_TOKEN_ENCRYPTION_KEY`; в БД — `ciphertext`, `nonce`, `key_version`. - AAD связывает ciphertext с `member_id`, portal domain и token type. - Поддерживается keyring current+previous для rolling rotation и re-encryption job. - Токены никогда не логируются, не экспортируются в metrics/traces и не возвращаются API. - DB/TLS и backup encryption остаются дополнительными слоями. ### 7.3. Refresh - refresh заранее, когда `expires_at - now <= skew` (ориентир 60 с); - single-flight на portal через DB advisory lock/lease; - POST только на allow-listed `https://oauth.bitrix.info/oauth/token/`; - refresh token rotation сохраняется атомарно; - при `expired_token` — максимум один refresh+replay; - `invalid_grant` переводит installation в `reauth_required`, readiness degraded, outbound fail-closed; - timeout/retry bounded; secret/client credentials не попадают в exception text. ### 7.4. Connector setup Используемые методы: - `imconnector.register`: `ID=han_mobile_app`, name/icon, `{BITRIX_PUBLIC_BASE_URL}/placement`; - `imconnector.activate`: connector, `LINE=8`, `ACTIVE=1`; - `event.bind`: `OnImConnectorMessageAdd`, `OnImConnectorDialogStart`, `OnImConnectorDialogFinish`; - `imconnector.status` для reconcile/readiness; - `imconnector.send.messages`; - `imconnector.send.status.delivery`. Каждый setup step хранит desired/observed state, attempts и safe error. Повтор не создаёт duplicate binding; если API Bitrix не гарантирует это, сначала проверяется status/list binding. ## 8. Webhook parsing и безопасность Поддерживаются JSON, form-urlencoded, multipart и PHP-style keys/числовые dict. Parser: - ограничивает body/header/field count, nesting, array/message count и строковые длины; - NFKC не применяется к opaque ids/tokens; - не сохраняет неизвестный raw body без redaction; - принимает только известные events; прочие безопасно `ignored` с metric; - проверяет connector `han_mobile_app`, line `8`, expected member/domain; - проверяет `auth.application_token` constant-time против расшифрованного portal token и/или `BITRIX_APPLICATION_TOKEN`; - не доверяет IP как единственной аутентификации, но nginx edge limit/allow policy дополняет token; - всегда отвечает достаточно быстро после durable insert, чтобы Bitrix retry не создал storm. Невалидный security token не маскируется как успешная обработка в telemetry: внешний ответ может быть нейтральным, но audit/metric фиксируют reject. Callback secret и payload не логируются. ## 9. Нормализация и inbox-контракт API Owned receiver находится в `api-backend`: ```text POST http://api-backend:8000/internal/openlines/v1/inbox Authorization: Bearer ${BITRIX_API_FORWARD_TOKEN} ``` Значение равно `BITRIX_API_INBOX_TOKEN` на API. `message.new`: ```json { "event_id": "stable-opaque", "event_type": "message.new", "external_chat_id": "uuid", "bitrix_message_id": "string", "occurred_at": "2026-07-10T09:00:00Z", "message": { "text": "Ответ оператора или пустая строка", "files": [{ "name": "scan.pdf", "mime_type": "application/pdf", "size_bytes": 12345, "download_url": "https://..." }] } } ``` `dialog.closed`: ```json { "event_id": "stable-opaque", "event_type": "dialog.closed", "external_chat_id": "uuid", "bitrix_message_id": null, "occurred_at": "2026-07-10T09:00:00Z", "message": null } ``` `event_id` обязателен согласно допущению module-01 A2; предпочтительно используется Bitrix event/message/session id, иначе versioned SHA-256 стабильных полей. `message.new` дополнительно unique по `(external_chat_id, bitrix_message_id)`. Пустые text+files отклоняются. URL файла передаётся только API; API защищается от SSRF, скачивает с лимитами и сохраняет в S3-data. Local app не скачивает/не хранит файл. ## 10. Delivery ack входящего события Критический инвариант: ```text Bitrix webhook → durable inbox → API 201/duplicate 200/204 → только затем imconnector.send.status.delivery ``` Ack запрещён при timeout/5xx/неприменённом `404` API. Если API commit успешен, но HTTP response потерян, повтор forward получает duplicate ack, после чего delivery status безопасно отправляется. Ack имеет собственный outbox/retry. Ошибка ack не повторяет application события в API. ## 11. Inbox, outbox, DLQ и backoff ### Inbox Webhook transaction сохраняет event, normalized payload/fingerprint и initial status. Worker использует `FOR UPDATE SKIP LOCKED`, lease и heartbeat. States: ```text received → forwarding → api_acked → ack_pending → completed ↘ retry received/forwarding/retry → dead_letter ``` ### Outbound messages States: `received | sending | delivered | retry | ambiguous | dead_letter`. Unique `message_id`; payload versioned; signed URLs не должны переживать TTL — при retry API обязан дать актуальный URL по согласованному recovery контракту либо операция уходит в reconciliation. ### Backoff - exponential full jitter, ориентир 1, 2, 4, 8… max 300 с; - учитывать `Retry-After` Bitrix/API; - max attempts и max age — infra env; - permanent 4xx/schema/auth не повторяются автоматически; - DLQ содержит safe error code, не token/raw PII; - replay — ops runbook/CLI с audit, не публичный endpoint MVP. ## 12. PostgreSQL `bitrix_local` Общие правила: UUID/timestamptz, schema-qualified DDL, soft delete для прикладных records, технические queue rows архивируются/удаляются по retention. Runtime role `bitrix_local_app`; отдельная migration role. Прямого доступа к `han_app` нет. ### 12.1. `portal_installations` `id`, `member_id` unique, `domain`, `client_endpoint`, encrypted token columns, `expires_at`, `scope`, `key_version`, `install_status`, `setup_status`, `last_refresh_at`, `last_error_code`, common fields. Indexes: unique active `member_id`; unique active normalized domain. MVP разрешает только один active expected portal. ### 12.2. `connector_setup` `id`, `portal_id`, `connector_id`, `line_id`, `registered`, `activated`, `bindings_json`, `desired_version`, `observed_at`, `next_retry_at`, `attempt_count`, lease/error fields. Unique `(portal_id, connector_id, line_id)`. ### 12.3. `dialog_sessions` `id`, `external_chat_id uuid`, `bitrix_chat_id bigint NULL`, `session_id varchar NULL`, `portal_id`, `status open|closed`, common fields. Indexes: - unique active `external_chat_id`; - index `(bitrix_chat_id) WHERE record_status='A'`; - index `(session_id)`; - `(status, updated_at)`. Связь с user_id не нужна: идентичность принадлежит App DB. ### 12.4. `inbox_events` `id`, `event_id`, `event_type`, `external_chat_id`, `bitrix_message_id`, `payload_fingerprint`, `normalized_json`, `status`, attempts/next/lease, `api_ack_status`, `delivery_ack_status`, safe error, timestamps. Unique `event_id`; unique partial `(external_chat_id, bitrix_message_id)`; worker index `(status,next_attempt_at)`. Raw payload хранится только если необходим для forensic, зашифрован/редактирован и с коротким retention; preferred — минимальный normalized payload. ### 12.5. `outbound_messages` `id`, `message_id uuid unique`, `external_chat_id uuid`, `request_fingerprint`, `payload_json`, `status`, `bitrix_message_id`, `response_json`, attempts/lease/error/timestamps. Index worker `(status,next_attempt_at)`. ### 12.6. `delivery_ack_outbox` Unique inbox event; status/attempt/next/lease, minimal Bitrix delivery DTO. Не содержит API token. ### 12.7. `install_runs` и `audit_events` Append-only setup step/results и security/ops actions без tokens/raw payload. BRIN/date indexes при росте. ## 13. Alembic и транзакции - Никакого `CREATE TABLE IF NOT EXISTS` при startup. - `alembic upgrade head` — отдельный deploy step. - Expand/migrate/contract, forward-fix; destructive migration только после backup/согласования. - Smoke upgrade пустой и предыдущей версии. - Внешний HTTP не выполняется внутри DB transaction. - Claim → commit lease → external call → finalize under row lock. - Setup/refresh используют portal-scoped lock. ## 14. Bitrix rate limits и resilience - Ограничить concurrency (начально 2 на portal) и локальный token bucket. - Разделить quotas setup, outbound, ack/status. - На Bitrix rate-limit учитывать headers/body code и `Retry-After`. - Circuit breakers отдельно: OAuth endpoint, portal REST, API forward. - Timeout: connect 3 с, обычный REST/read 10–15 с, OAuth 10 с; значения infra env. - 4xx domain/schema не открывает circuit; 429/transient/timeout учитываются по policy. - Half-open имеет один probe; retry storms предотвращаются jitter/queue concurrency. - Один `httpx` pool; TLS verify обязателен; redirects для token/REST запрещены либо allow-listed. ## 15. Health `GET /health/live`: только процесс/event loop, `200`. `GET /health/ready` с коротким timeout проверяет: - PostgreSQL `SELECT 1`, expected Alembic revision; - usable active portal OAuth либо сообщает `portal_not_installed`; - connector desired state register+line 8+bindings; - workers heartbeat/lease; - backlog age/DLQ thresholds; - forward URL/token configured; - circuit state. DB/schema failure → `503`. До install сервис может быть `200 degraded` или `503 portal_not_installed` согласно ops policy; для production traffic выбран `503`, liveness остаётся 200. Ответ не делает внешних Bitrix calls на каждый probe — использует свежий cached observed state. ## 16. Observability JSON fields: timestamp, level, `service.name=bitrix-local-app`, module, event, request_id, trace/span id, route, event_type, portal hash/member hash, message/event id hash, attempt, queue age, dependency, status/error code, duration. Не логируются OAuth/application/service tokens, Authorization, raw callback, message text, phone/email/name, filenames с PII, file/download URL, response body Bitrix. Metrics: - HTTP latency/status; - callback accepted/rejected/duplicate; - parser variants/errors; - OAuth refresh success/failure/time-to-expiry; - connector setup desired/observed; - outbound success/retry/ambiguous/DLQ; - inbox depth/oldest age/retry/DLQ; - API forward and delivery ack; - Bitrix REST latency/rate-limit/circuit; - DB pool/lease/readiness. IDs не metric labels. Trace context передаётся в API; внешний Bitrix call — child span без token/query. ## 17. Security - TLS boundary — root nginx; internal HTTP только backend network. - Internal endpoints не edge-routed, Bearer token обязателен. - Exact host/domain/connector/line allow-list. - `client_endpoint` из callback валидируется против portal allow-list для защиты SSRF. - Strict DTO/body limits; parameterized SQL. - OAuth encryption+key rotation; secrets только env/secret mount. - Non-root, read-only root fs, tmpfs, dropped capabilities. - OpenAPI UI off production; committed OpenAPI 3.1 обязателен. - CORS не нужен; placement не получает secrets. - Error envelope не раскрывает host/stack/raw dependency response. - Dependency/image scanning и pinned lock/image. ## 18. Env Канонические из arch-04: ```text BITRIX_DATABASE_URL BITRIX_CLIENT_ID BITRIX_CLIENT_SECRET BITRIX_CONNECTOR_ID=han_mobile_app BITRIX_CONNECTOR_NAME=HAN Mobile App BITRIX_OPEN_LINE_ID=8 BITRIX_PUBLIC_BASE_URL=https://tohin.ru/bitrix BITRIX_APPLICATION_TOKEN BITRIX_INTERNAL_API_TOKEN BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox BITRIX_API_FORWARD_TOKEN OTEL_EXPORTER_OTLP_ENDPOINT APP_ENV LOG_LEVEL ``` Предлагаемые infra env, которые до реализации нужно добавить в arch-04: ```text BITRIX_TOKEN_ENCRYPTION_KEY BITRIX_TOKEN_ENCRYPTION_KEY_VERSION BITRIX_HTTP_TIMEOUT_SEC=15 BITRIX_HTTP_MAX_CONCURRENCY=2 BITRIX_RETRY_MAX_ATTEMPTS=10 BITRIX_RETRY_MAX_DELAY_SEC=300 BITRIX_INBOX_RETENTION_DAYS BITRIX_DLQ_ALERT_AGE_SEC ``` Business settings здесь не хранятся. Legacy `BITRIX_SYNC_FORWARD_*` удаляются после migration window и не являются каноническими. ## 19. Docker и deployment - service `bitrix-local-app`, `expose: 8080`, без `ports`; - networks `backend`,`observability`; root nginx отдельно; - managed PostgreSQL вне compose, TLS обязательно; - нет SQLite volume production; - healthcheck `/health/live`; readiness — orchestration/monitoring; - migration one-shot job до rollout; - graceful shutdown: stop claims, finish in-flight до grace, release leases, close pools; - stateless filesystem. Прототипные `deploy/nginx/*`, certbot/SSL scripts и отдельный compose-stack не переносятся: сертификат и routing принадлежат корневому nginx/compose. ## 20. Что переиспользуется из прототипа Концептуально переиспользуются и покрываются новыми тестами: - tolerant parser JSON/form/multipart и `auth[...]`; - преобразование PHP-style `MESSAGES` list/dict; - разделение client/connector/handler/normalizer/session store; - setup `register → activate → bind`; - refresh до expiry и один replay `expired_token`; - extraction session `CHAT_ID`/`ID`; - `application_token` и Bearer constant-time compare; - deterministic idempotency event key как основа fingerprint; - `dialog_sessions` и enrichment; - отключение docs production; - различение Open Lines и CRM sync. Обязательно меняется: - `/internal/v1/*` → только `/internal/openlines/v1/*`; - forward envelope → канонический `POST /internal/openlines/v1/inbox`; - immediate delivery ack до API запрещён; - single-attempt forward → durable worker/backoff/DLQ; - plaintext tokens → AEAD encryption/key rotation; - sync psycopg2/thread lock → async pool/transactions/leases; - SQLite и DDL-on-start не используются production; - отдельный nginx/certbot/compose удаляются из production topology; - `/bitrix-internal/` edge alias не нужен: internal API не публикуется; - raw payload/error storage/logging минимизируется; - uninstall деактивирует installation; - Alembic и OpenAPI 3.1 обязательны. ## 21. Тестовая матрица ### Unit - parser variants/nesting/limits; - normalizer message/file/start/finish; - token encryption/decryption/AAD/rotation; - fingerprint/idempotency; - session extraction variants; - retry classification/backoff/jitter; - URL/portal validation and redaction. ### Integration - Alembic empty/upgrade; - concurrent duplicate webhook; - outbound same/different fingerprint; - `SKIP LOCKED`, lease expiry, crash recovery; - refresh single-flight; - setup reconcile; - DB constraints/soft delete; - no DDL at startup. ### Contract - all public/internal schemas in committed OpenAPI; - tokens and paired names with module-01; - `message.new`/`dialog.closed` inbox; - API 201/duplicate before delivery ack; - request-id/trace propagation; - Bitrix fixture payloads and response variants. ### E2E/failure - install portal → connector visible on line 8; - text/file send and mapping; - operator text/file → API → ack; - duplicate/reordered callbacks; - API outage, Bitrix 429/5xx/timeout, OAuth expiry/invalid_grant; - crash at every checkpoint; - DLQ/replay; - circuit half-open; - logs contain no secrets/PII/URLs. ## 22. Definition of Done - portal/connector/line fixed and validated; - public and internal paths exactly match arch-02; - internal API is unreachable from public edge; - install/OAuth encryption/refresh/setup reconciliation complete; - outbound idempotency survives crash/ambiguous response; - inbox retry/DLQ and ack-after-API invariant proven; - operator text/files and `dialog.closed` contract-tested; - `bitrix_local` schema, indexes and Alembic migrations tested; - rate-limit/circuit/timeout/graceful shutdown implemented; - health/metrics/traces/JSON logs secure; - OpenAPI 3.1 committed and parity checked; - root Compose starts non-root container without published port; - runbooks: reinstall, key rotation, OAuth failure, setup retry, backlog/DLQ, migration/rollback; - no SQLite, startup DDL or separate production nginx. ## 23. Решения, допущения и TBD **Решения:** - B1: canonical internal prefix только `/internal/openlines/v1`. - B2: delivery ack только после API commit/duplicate ack. - B3: OAuth tokens шифруются application-level AEAD. - B4: durable PostgreSQL inbox/outbox/DLQ; Redis не требуется. - B5: `external_chat_id=dialog_id`; local app не хранит user profile. - B6: production только managed PostgreSQL + Alembic. **Допущения:** - A1: один active portal `han0107.bitrix24.ru` в MVP. - A2: Bitrix fixtures позволят стабильно извлечь event/message/session ids; иначе versioned fingerprint. - A3: API может повторно выдать актуальный signed file URL при delayed outbound recovery; exact handshake требуется в contract test. **TBD:** - B-TBD1: точные Bitrix REST quotas/headers и safe retry матрица по официальной документации/portal tests. - B-TBD2: окончательный outbound DTO user display name и file fields в OpenAPI. - B-TBD3: exact stable `event_id` для dialog events (согласовать с module-01 TBD-3). - B-TBD4: retention/RPO/RTO и DLQ replay authorization. - B-TBD5: encryption key source/rotation runbook до production. - B-TBD6: точный CSP `frame-ancestors` placement. - B7: operator files считаются trusted-channel данными MVP; api-backend применяет MIME/size/audit без Message Safety/AV, residual malware risk принят.