Files
vmall/openspec/changes/archive/2026-09-17-replace-mock-api-wave-2/design.md
T
james 2136a48fbe 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.
2026-09-17 16:36:10 +00:00

4.2 KiB

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.