Files

73 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 доступен.