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

This commit is contained in:
mi
2026-08-14 15:42:45 +03:00
parent e06a77ee1d
commit bbef7a30c9
521 changed files with 2597 additions and 2302 deletions
@@ -0,0 +1,643 @@
# 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"
},
"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"},
"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 по истечении необходимости;
- PII профиля не требуется; телефон/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`, `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 1015 с, 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 принят.