73 lines
10 KiB
Markdown
73 lines
10 KiB
Markdown
## Context
|
||
|
||
См. мотивацию в `proposal.md` и поведение в `specs/tbank-connection/spec.md`. После `add-authentication` backend имеет user-scoped SQLite-модель, Bearer guard, OpenAPI и Problem Details filter; `/settings` пока содержит только logout. Принятые ADR 003--005 уже устанавливают server-side gRPC boundary, один зашифрованный token на User и per-instance upstream resilience.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
|
||
- Хранить один проверенный T-Bank token на User, не раскрывая его за server boundary.
|
||
- Добавить минимальный authenticated HTTP contract и UI настроек для lifecycle connection.
|
||
- Централизовать TLS gRPC-вызов, лимитирование, retry и безопасную классификацию ошибок до появления read-paths.
|
||
|
||
**Non-Goals:**
|
||
|
||
- Получение и отображение счетов, портфеля, инструментов либо их cache; это задачи `add-brokerage-accounts` и `add-portfolio-view`.
|
||
- Key rotation, distributed rate limit/cache, persistent очередь, health-check token или serialisation параллельных изменений connection.
|
||
- SDK T-Bank, автоматическое обновление protobuf contracts и поддержка нескольких T-Bank connections у одного User.
|
||
|
||
## Decisions
|
||
|
||
### Модель connection и HTTP resource
|
||
|
||
Prisma добавляет `TbankConnection` с уникальным `userId`, `encryptedToken`, `connectedAt` и relation к `User`. Отдельная сущность выражает агрегат User--T-Bank Connection и не смешивает secret с profile. `GET` читает только эту запись. `PUT` сначала trims и проверяет token, затем создаёт или заменяет запись и устанавливает новый `connectedAt`. `DELETE` удаляет запись идемпотентно; сейчас user-scoped T-Bank cache отсутствует, поэтому purge является no-op до его появления.
|
||
|
||
Без serialisation, optimistic version или conditional writes параллельный `PUT`/`DELETE` разрешается порядком завершения запросов. Это намеренное MVP-ограничение: блокировки на User и public versioning усложняют только редкий конфликт между сессиями.
|
||
|
||
Альтернативы: поле token в `User` смешивает профиль и внешнюю credential; отдельные create/replace endpoints дублируют lifecycle единственного resource; `404` без connection делает нормальное UI-состояние исключением.
|
||
|
||
### Шифрование token
|
||
|
||
`TOKEN_ENCRYPTION_KEY` обязателен при старте, base64-декодируется ровно в 32 bytes. Backend использует встроенный Node `crypto` AES-256-GCM: random 12-byte nonce, ciphertext и 16-byte authentication tag хранятся в одном base64 payload с version byte. Расшифровка выполняется только непосредственно перед формированием `Authorization: Bearer` для gRPC metadata; token и metadata никогда не логируются.
|
||
|
||
При невалидном ключе backend не запускается. Key rotation не поддерживается: новый ключ требует отдельной миграционной стратегии. Это минимальнее и безопаснее, чем неявно хранить несколько ключей или plaintext fallback.
|
||
|
||
### T-Bank gRPC boundary
|
||
|
||
Backend vendoring-ит полный набор `.proto` contracts из `references/moex-vibe` (`common`, `users`, `operations`, `instruments` и их imports); источник последующих ручных обновлений -- официальный T-Bank Invest API. Эта change загружает и использует только `UsersService/GetAccounts` с `ACCOUNT_STATUS_OPEN` для проверки token. Полный набор сохраняет согласованную contract tree для следующих capabilities, не добавляя сейчас operations/instruments client surface.
|
||
|
||
`@grpc/grpc-js` и `@grpc/proto-loader` создают TLS client к `TBANK_GRPC_ENDPOINT` (default `invest-public-api.tbank.ru:443`). Системное trust store используется по умолчанию; необязательный `TBANK_CA_CERT_PATH` задаёт читаемый PEM root CA для сетей с TLS interception. TLS verification отключать нельзя. Один gRPC attempt имеет deadline 10 seconds.
|
||
|
||
Альтернативы: готовый SDK расширяет dependency surface и не нужен одному RPC; копирование только `users.proto` затрудняет согласованное продолжение T-Bank integration; custom CA как обязательная настройка ломает обычное публичное TLS-подключение.
|
||
|
||
### Upstream policy и error mapping
|
||
|
||
Единый in-memory FIFO limiter на экземпляр запускает не более 5 T-Bank RPC за секунду и содержит не более 50 ожидающих вызовов. Переполненный limiter сразу возвращает `TBANK_UNAVAILABLE`, не вызывая upstream. `UNAVAILABLE`, `DEADLINE_EXCEEDED` и `RESOURCE_EXHAUSTED` повторяются ровно один раз спустя 1 second, вновь проходя limiter. Остальные ошибки не повторяются.
|
||
|
||
`UNAUTHENTICATED` и `PERMISSION_DENIED` преобразуются в `400 INVALID_TBANK_TOKEN`. После исчерпания retry, overflow очереди и все прочие gRPC ошибки преобразуются в `503 TBANK_UNAVAILABLE`; подробности upstream остаются только в безопасных server logs без token. Проверка не меняет connection, пока gRPC-вызов не завершён успешно.
|
||
|
||
Фиксированные 5 RPS, queue 50, delay 1 second и deadline 10 seconds соответствуют MVP policy. Отдельные лимиты сервисов, burst, exponential backoff, jitter и distributed coordination не добавляются до появления нагрузки или нескольких экземпляров.
|
||
|
||
### Frontend settings flow
|
||
|
||
Новая FSD feature управляет transport calls только через generated OpenAPI client и React Query. Settings page сначала получает connection status и показывает отдельное retryable error state при неуспехе. Loaded state показывает read-only token hint, password field и действие `Подключить` или `Заменить`; token очищается после успешного `PUT`, но сохраняется при error. При подключении отображается локализованный `connectedAt` из RFC 3339 UTC.
|
||
|
||
Удаление требует Mantine confirmation modal. Пока `PUT` или `DELETE` pending, section field, buttons и modal actions disabled. UI различает `INVALID_TBANK_TOKEN` и `TBANK_UNAVAILABLE` локализованными сообщениями; остальные ошибки получают общий безопасный текст.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- Per-instance limiter не координирует несколько backend instances → MVP рассчитан на один instance; distributed limiter добавляется перед horizontal scaling.
|
||
- Параллельные mutation requests могут примениться в порядке завершения → accepted MVP limitation; добавить User-scoped lock или versioning при появлении конфликтов.
|
||
- Token может быть отозван после сохранения, а status endpoint этого не определит → будущие accounts/portfolio read-paths классифицируют upstream error; настройки не тратят quota на health check.
|
||
- Невозможность расшифровать данные после ручной смены key блокирует token → key rotation требует отдельной миграции до такой операции.
|
||
|
||
## Migration Plan
|
||
|
||
1. Добавить gRPC dependencies, vendored protobuf contracts и fail-fast environment validation для encryption key, endpoint, rate limit и optional CA path.
|
||
2. Добавить Prisma migration для `TbankConnection`, crypto service, T-Bank client и authenticated connection HTTP endpoints с OpenAPI.
|
||
3. Regenerate typed frontend API client из backend OpenAPI и реализовать settings feature с confirmation modal.
|
||
4. Добавить минимальные backend unit/e2e checks: crypto round-trip и invalid key, failed `PUT` не сохраняет token, `DELETE` удаляет запись, endpoints требуют auth и не возвращают token. Проверить UI вручную.
|
||
5. Создать `docs/features/tbank-connection.md` с API, configuration, RFC 3339 UTC, resilience policy и Mermaid sequence diagram; выполнить lint, tests, builds и OpenSpec validation.
|
||
|
||
Rollback до release: откатить приложение и Prisma migration. После сохранения реальных token rollback должен выполняться только вместе с безопасным удалением или сохранением зашифрованной таблицы, пока исходный encryption key доступен.
|