docs(specs): archive auth and cache remediation
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-23
|
||||
@@ -0,0 +1,104 @@
|
||||
## 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 не меняются.
|
||||
@@ -0,0 +1,45 @@
|
||||
## Why
|
||||
|
||||
Backend допускает неограниченный рост process-local кэша страниц журнала и
|
||||
теряет детерминированность при конкурентных регистрации и refresh. Эти
|
||||
подтверждённые audit findings WL-P2-01—03 могут приводить к исчерпанию памяти,
|
||||
`500` вместо ожидаемого конфликта и отзыву успешно обновлённой Session.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Заменить неограниченный cache страниц Account Ledger на bounded
|
||||
`lru-cache`: не более 100 страниц, TTL 30 секунд, LRU eviction и
|
||||
канонический cache key.
|
||||
- Сделать database unique constraint источником истины для параллельной
|
||||
регистрации и преобразовывать Prisma `P2002` в `409
|
||||
EMAIL_ALREADY_REGISTERED`.
|
||||
- Ротировать refresh token compare-and-swap обновлением Session, чтобы
|
||||
только один конкурентный запрос был успешным; superseded token отвечает
|
||||
`401` без отзыва Session победителя.
|
||||
- Добавить детерминированные service-level regression tests и обновить
|
||||
устойчивую auth/cache документацию, не объявляя временно неработающий
|
||||
Prisma e2e gate восстановленным.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
<!-- None. -->
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `user-authentication`: определяет детерминированные результаты
|
||||
конкурентной регистрации и refresh rotation, включая поведение superseded
|
||||
refresh token.
|
||||
- `brokerage-account-ledger`: определяет bounded process-local cache страниц
|
||||
журнала и его каноническую идентичность.
|
||||
|
||||
## Impact
|
||||
|
||||
- Backend: `AuthService`, `BrokerageOperationsService`, unit tests и
|
||||
production dependency `lru-cache`.
|
||||
- Specs: delta specifications для user authentication и brokerage account
|
||||
ledger.
|
||||
- Docs: ADR-009 и фактическая документация authentication/ledger cache.
|
||||
- Public HTTP routes и auth success statuses не меняются; их contract
|
||||
alignment остаётся отдельным WL-P2-04.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Кэш страниц журнала ограничивает локальные ресурсы
|
||||
Система SHALL кэшировать не более 100 недавно использованных страниц Account Ledger в каждом backend process не более чем на 30 секунд. Cache key MUST включать User, Brokerage Account, нормализованные период и categories, opaque cursor и limit; порядок или дублирование categories MUST NOT создавать отдельную cache identity, а отсутствующий конец периода MUST использовать текущий 30-секундный bucket. Просроченная страница MUST NOT возвращаться, а при переполнении cache MUST удалять наименее недавно использованную страницу.
|
||||
|
||||
#### Scenario: Равнозначные фильтры используют одну cache identity
|
||||
- **WHEN** User в одном 30-секундном bucket повторяет запрос страницы журнала с теми же периодом, cursor и limit, но с categories в другом порядке или с дубликатами
|
||||
- **THEN** система использует ту же cache identity и не запрашивает upstream повторно для свежей страницы
|
||||
|
||||
#### Scenario: Большое число уникальных запросов не увеличивает cache без границы
|
||||
- **WHEN** backend обслуживает больше 100 уникальных запросов страниц журнала
|
||||
- **THEN** cache содержит не более 100 страниц и освобождает наименее недавно использованные страницы
|
||||
|
||||
#### Scenario: Просроченная страница не возвращается
|
||||
- **WHEN** cached страница журнала старше 30 секунд
|
||||
- **THEN** система не возвращает её как свежий результат и получает актуальную страницу из upstream
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Регистрация пользователя
|
||||
Система SHALL позволять зарегистрировать User по уникальному нормализованному email и паролю. Email MUST нормализоваться удалением внешних пробелов и приведением к нижнему регистру; он MUST содержать не более 254 символов, ровно один `@`, непустые local и domain части и не содержать пробельных или управляющих символов. Пароль MUST передаваться без изменения, содержать от 12 до 72 UTF-8 байтов и не иметь требований к составу символов. Успешная регистрация MUST создать первую Session и вернуть профиль `{ id, email }` и access token.
|
||||
|
||||
#### Scenario: Успешная регистрация
|
||||
- **WHEN** пользователь отправляет новый email и валидный пароль
|
||||
- **THEN** система создаёт User и Session, устанавливает refresh-cookie и возвращает access token с нормализованным профилем
|
||||
|
||||
#### Scenario: Нормализация email при регистрации
|
||||
- **WHEN** пользователь регистрирует ` Anna@Example.com `
|
||||
- **THEN** система сохраняет и возвращает email `anna@example.com`
|
||||
|
||||
#### Scenario: Повторная регистрация
|
||||
- **WHEN** пользователь регистрирует email, уже принадлежащий User после нормализации
|
||||
- **THEN** система отвечает `409` Problem Details с code `EMAIL_ALREADY_REGISTERED`
|
||||
|
||||
#### Scenario: Конкурентная регистрация
|
||||
- **WHEN** два запроса одновременно регистрируют один нормализованный email
|
||||
- **THEN** ровно один запрос создаёт User и Session, а второй отвечает `409` Problem Details с code `EMAIL_ALREADY_REGISTERED`
|
||||
|
||||
#### Scenario: Невалидные регистрационные данные
|
||||
- **WHEN** пользователь отправляет email или пароль, не соответствующие правилам регистрации
|
||||
- **THEN** система отвечает `400` Problem Details с code `VALIDATION_ERROR` и указателями на невалидные поля
|
||||
|
||||
### Requirement: Access и refresh сессии
|
||||
Система SHALL выдавать access token сроком 15 минут и refresh token сроком 7 дней. Access token MUST передаваться клиенту только в ответе API и использоваться как Bearer token. Refresh token MUST передаваться только в `httpOnly`, `SameSite=Lax`, host-only cookie `refresh_token` с path `/api/v1/auth`; в HTTPS-среде cookie MUST иметь атрибут `Secure`. Каждый новый refresh token MUST иметь уникальный JWT ID и ротироваться при успешном обновлении Session; Session MUST оставаться активной 7 дней после последнего успешного обновления. Одновременные Session одного User MUST быть независимы и не иметь числового лимита.
|
||||
|
||||
#### Scenario: Успешное восстановление сессии
|
||||
- **WHEN** клиент отправляет действительный refresh-cookie
|
||||
- **THEN** система заменяет refresh-cookie и возвращает новый access token и профиль Session
|
||||
|
||||
#### Scenario: Конкурентное восстановление сессии
|
||||
- **WHEN** два запроса одновременно отправляют один действительный refresh-cookie одной Session
|
||||
- **THEN** ровно один запрос заменяет refresh-cookie и возвращает новый access token и профиль, второй отвечает `401` Problem Details с code `UNAUTHORIZED`, а Session победителя остаётся активной
|
||||
|
||||
#### Scenario: Истёкшая сессия
|
||||
- **WHEN** клиент отправляет refresh-cookie Session, не обновлявшейся более 7 дней
|
||||
- **THEN** система удаляет refresh-cookie и отвечает `401` Problem Details с code `UNAUTHORIZED`
|
||||
|
||||
#### Scenario: Повторное использование refresh token
|
||||
- **WHEN** клиент отправляет refresh token, заменённый предыдущим успешным refresh
|
||||
- **THEN** система удаляет refresh-cookie и отвечает `401` Problem Details с code `UNAUTHORIZED`, не отзывая связанную Session только из-за этого запроса
|
||||
@@ -0,0 +1,23 @@
|
||||
## 1. Bounded cache страниц журнала
|
||||
|
||||
- [x] 1.1 Добавить в `BrokerageOperationsService` failing regression tests для canonical categories/default `to` bucket, сохранения ownership checks и eviction после 100 уникальных страниц; не добавлять test-only clock seam для TTL third-party library.
|
||||
- [x] 1.2 Добавить `lru-cache` как прямую production dependency и реализовать domain-local Ledger Page Cache с TTL 30 секунд, max 100 и canonical key без обхода connection/account ownership checks.
|
||||
- [x] 1.3 Запустить focused cache spec и backend unit suite; устранить каждый regression failure.
|
||||
|
||||
## 2. Детерминированная auth concurrency
|
||||
|
||||
- [x] 2.1 Создать `AuthService` service-level tests с stateful Prisma fake для P2002 loser при параллельной регистрации и ожидаемого `409 EMAIL_ALREADY_REGISTERED`.
|
||||
- [x] 2.2 Добавить barrier-based CAS test двух refresh одного token: один победитель, один `401`, predicate содержит id и old refresh hash, Session победителя не отзывается.
|
||||
- [x] 2.3 Преобразовать ожидаемый Prisma P2002 от `user.create` в существующий `ConflictException`, не скрывая остальные database errors.
|
||||
- [x] 2.4 Реализовать refresh rotation через conditional `updateMany`; при CAS loser вернуть `UNAUTHORIZED` без revoke Session и сохранить lifecycle cookie через существующий controller boundary.
|
||||
- [x] 2.5 Запустить focused auth spec и backend unit suite; устранить каждый regression failure.
|
||||
|
||||
## 3. Постоянная документация
|
||||
|
||||
- [x] 3.1 Довести ADR-009, `CONTEXT.md`, feature documentation authentication и brokerage account ledger до фактически реализованных cache и refresh policies без создания параллельных SDD-артефактов в docs.
|
||||
|
||||
## 4. Verification до archive
|
||||
|
||||
- [x] 4.1 Выполнить backend non-mutating gates под Node 22: `npm run lint`, `npm run typecheck:app`, `npm run typecheck:test` и `npm test -- --runInBand`; не выдавать deferred `test:e2e` за passing gate.
|
||||
- [x] 4.2 Выполнить `openspec validate harden-auth-and-cache-concurrency --type change --strict --store wealth-ledger-specs`, strict validation synced specs, authority verifier и `git diff --check` во всех затронутых repositories.
|
||||
- [x] 4.3 Выполнить applicable pre-archive `opsx-verify` procedure и independent read-only review implementation, delta specs, документации и scope; устранить каждый blocking result до archive.
|
||||
Reference in New Issue
Block a user