644 lines
30 KiB
Markdown
644 lines
30 KiB
Markdown
# 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`](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), прототип [`../../HAN_chat/bitrix-local-app/README.md`](../../HAN_chat/bitrix-local-app/README.md) и [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py).
|
||
|
||
## 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`;
|
||
- изменение `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 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 принят.
|