Files
specs/openspec/changes/archive/2026-08-23-harden-auth-and-cache-concurrency/design.md
T

105 lines
6.6 KiB
Markdown
Raw 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` и изменяемое поведение в delta specs. Backend
использует Nest 11, Prisma 7 с SQLite adapter, `AuthService` и локальные
`Map`-кэши. Публичные auth routes, cookie attributes и success statuses не
меняются; backend e2e migration gate остаётся отложенным по WL-P2-05.
## Goals / Non-Goals
**Goals:**
- Сделать cache страниц журнала ограниченным и полезным для семантически
равных запросов.
- Устранить race между проверкой уникальности email и его созданием.
- Гарантировать единственного победителя refresh rotation без случайного
revoke его Session.
- Оставить воспроизводимые service-level tests, пока canonical SQLite e2e
path недоступен.
**Non-Goals:**
- Изменять auth response statuses, OpenAPI snapshot или frontend client
resilience (WL-P2-04, WL-P2-07 и WL-P2-08).
- Добавлять byte-weight cache budget, распределённый cache, cache
invalidation или cross-instance coherence.
- Добавлять auth framework, новую схему Session, grace window либо отдельную
историю использованных refresh token.
- Объявлять `test:e2e` или общий workspace verdict восстановленными.
## Decisions
### Bounded cache остаётся domain-local
`BrokerageOperationsService` получает прямую production dependency
`lru-cache` и хранит в ней максимум 100 response pages с TTL 30 секунд.
Это использует готовую, тестируемую LRU policy без глобального Nest cache
layer. Cache identity строится отдельно из user/account и нормализованных
effective query parameters: categories сортируются и deduplicate-ятся,
cursor остаётся opaque, а отсутствующий `to` привязывается к текущему
30-секундному bucket. Cache никогда не заменяет существующую проверку
connection и account ownership.
Альтернативы: `Map` с ручной eviction повторяет сложную policy; Nest
`CacheModule` потребует глобального HTTP keying и не подходит user/account
domain cache; byte budget не нужен без подтверждённого memory pressure.
### Database constraint определяет регистрацию
`AuthService.register` сохраняет предварительную проверку как UX
оптимизацию, но считает `user.create` authoritative. Только ожидаемый Prisma
unique-constraint error `P2002` от этой операции преобразуется в
`ConflictException` с существующим code `EMAIL_ALREADY_REGISTERED`; прочие
ошибки остаются внутренними. Это устраняет check-then-create race, не меняя
HTTP filter или API surface.
### Refresh rotation использует compare-and-swap
После проверки текущей Session сервис формирует новую token pair и обновляет
Session через `updateMany` с predicate `{ id, refreshTokenHash: oldHash }`.
`count === 1` означает победителя и разрешает вернуть новую пару. `count ===
0` означает superseded token: сервис очищает cookie через существующий
controller path и отвечает `401 UNAUTHORIZED`, не вызывая revoke Session.
Схема не меняется: current hash уже является достаточным CAS predicate.
Альтернативы: transaction добавляет SQLite locking/retry semantics без
дополнительной пользы; version column или token history нужны только для
отдельного различения late replay и concurrent loser. ADR-009 фиксирует
выбранный trade-off.
### Tests моделируют arbitration, не подменяют e2e
Новый `AuthService` spec использует stateful Prisma fake и barrier, чтобы
две регистрации одновременно дошли до `create`, а два refresh — до CAS.
Тесты проверяют наблюдаемые service outcomes и predicate CAS, включая
отсутствие revoke у loser. `BrokerageOperationsService` spec использует fake
upstream для проверки canonical hit, ownership checks и max-100 eviction;
TTL expiry обеспечивается и тестируется библиотекой `lru-cache`, поэтому
change не добавляет test-only clock seam. Это детерминированные regression
tests; они не выдают себя за HTTP/SQLite e2e и не меняют deferred status
WL-P2-05.
## Risks / Trade-offs
- [Superseded token может быть поздним replay, а не concurrent loser] →
`401` не отзывает Session по ADR-009; более строгая дифференциация требует
отдельного security change.
- [Одна cached page может быть большой] → максимальное число страниц
ограничивает cardinality; byte budget остаётся отдельным будущим решением.
- [TTL не создаёт distributed coherence] → cache остаётся process-local,
как и текущий local MVP policy.
- [Unit tests не доказывают SQLite migration/HTTP wiring] → они покрывают
сервисную логику до recovery trigger для WL-P2-05.
## Migration Plan
1. Добавить прямую зависимость `lru-cache` и service-level tests.
2. Внести cache, registration и CAS изменения без Prisma migration.
3. Обновить фактические feature docs и ADR-009; сохранить WL-P2-05/P2-13
deferred в remediation registry до их отдельных trigger.
4. Выполнить backend non-mutating gates, OpenSpec verification и
independent review до archive.
Rollback заключается в откате приложения и dependency lockfile; schema и
публичные route shapes не меняются.