Правки от GPT
This commit is contained in:
@@ -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`](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.
|
||||
- B-TBD7: antivirus policy operator files остаётся у `api-backend`.
|
||||
Reference in New Issue
Block a user