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

6.6 KiB
Raw Blame History

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 не меняются.