chore(openspec): archive the mock-migration waves and green the spec set

Archive the three completed changes behind replace-mock-api-wave-1/2/3. Each
merge applied cleanly to the main specs:

- catalog gains the Public product browse requirement (subtree filtering and
  price sort)
- frontend-mall picks up the per-domain adapter, the pinned home page, the
  discovery-page changes, the live auth panels and the live transaction flows
- cart's Server-side cart requirement now documents the shop and stock carried
  by every line

Also replace the TBD Purpose placeholder in all eleven specs with a one-line
description of what each capability covers. Those placeholders predate this
work and were the only reason `openspec validate --all --strict` reported
0 passed / 11 failed; it now reports 11 passed / 0 failed.
This commit is contained in:
2026-09-17 16:36:10 +00:00
parent e1a0a5dbdb
commit 2136a48fbe
28 changed files with 84 additions and 30 deletions
@@ -0,0 +1,60 @@
# Design
## Context
See `proposal.md` — Why. After wave 1 the mall selects adapters per domain through `liveDomains` (`apps/mall/plugins/api.ts`); `auth`, `cart`, `orders`, `shipments` and `invoices` are what remain on fixed data.
Session truth today lives entirely in `localStorage`: `vmall.token` is read by the plugin's `getToken`, `vmall.user` is read by `middleware/auth.ts`, and both are written by `stores/session.ts`. The fixed-data `me()` returns a constant `MOCK_USER`, so nothing ever proves a stored token is still valid.
The live contract this wave targets (`apps/api/src/routes/auth.rs`):
| Endpoint | Behaviour |
|---|---|
| `POST /auth/register` | 400 on invalid email, password under 8 characters, or blank display name; **409** on duplicate email |
| `POST /auth/login` | **401** on bad credentials; returns `{ token, user }` |
| `GET /auth/me` | requires a bearer token; returns the `User` |
`ApiError` already carries `status` and `code` (`packages/shared/src/api.ts:26-34`).
## Goals / Non-Goals
**Goals:**
- Credentials, roles and tokens become the real user's, with failures reported distinctly.
- The flip stays confined to the `auth` domain, so every other domain keeps working.
**Non-Goals:**
- No refresh-token or cookie/session-server move: the JWT stays in `localStorage`.
- No password-reset backend; `/forgot-password` stays presentational, which is a known gap rather than something this wave fixes.
- No change to what the buyer center displays — its data is still fixed.
## Decisions
**1. Validate a restored token once per load through `me()`, and only clear the session on 401/403.**
`middleware/auth.ts` currently trusts `localStorage`, so a stale or forged token reaches protected pages and fails later, further from the cause. A single `me()` call on hydrate puts the check where the session is established.
*Alternatives:* a 401 interceptor inside the shared `request()` helper (touches all three apps — a shared-package change outside this wave); validating in every guarded page (one call per navigation).
A network failure is deliberately **not** treated as a rejected token, so an unreachable API does not silently log the shopper out.
**2. The 8-character rule lives in the panel and mirrors the API.**
The API is the authority; the client check exists only to avoid a pointless round trip and a generic error. Relaxing the API to accept 6 would weaken the backend and needs a change outside this wave.
**3. Errors are mapped on `ApiError.status`/`code`, never on message text.**
Matching the API's prose would break whenever its wording changes. New strings go in `apps/mall/locales-extra.ts`, because AGENTS.md reserves the shared locale bundle for contract changes.
**4. The register panel drops the verification-code field.**
It is a mandatory no-op: the button only starts a countdown, the value is never sent, and no endpoint issues a code. Keeping it would demand input the API never checks.
**5. `liveDomains` gains `auth`; rollback stays a one-string edit.**
The fixed-data adapter keeps serving auth, so removing the entry restores the previous behaviour with no data migration.
## Risks / Trade-offs
- [A 401 from a still-mocked domain could be mistaken for a session failure] → only `me()` drives validation; cart, orders and invoices stay fixed-data and never answer 401.
- [Validation adds a round trip before the session is usable] → it runs only when a stored token exists, and it replaces blind trust rather than adding a second source of truth.
- [The demo loses "any password works"] → intended and marked **BREAKING**; the seeded `customer@vmall.local` account still demonstrates sign-in.
- [The register panel becomes shorter than the backend's own validation] → the panel mirrors the API rule, and the API still enforces it independently.
## Migration Plan
1. Add `auth` to `liveDomains` and land the panel, session and middleware changes in the same commit, so no user can submit the old 6-character rule against the new API.
2. Verify against the running backend with the seeded customer, then a wrong password, then a tampered token.
3. Rollback: drop `auth` from `liveDomains`. The fixed-data adapter still serves the domain, and no stored data needs reverting.