Files
specs/openspec/changes/archive/2026-08-12-add-tbank-connection/design.md
T

10 KiB
Raw Blame History

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 доступен.