# Frontend Phase 1 — Auth: phone-OTP login & role routing

> **Mission:** make people able to actually sign in. Replace the inherited username/password stub with
> Balinyaar's real credential — **phone-OTP** — and wire the gate that every other screen sits behind.
> Build the customer login (A1), the OTP screen (A2), the family↔nurse login switch (B1/B2 entry), and
> the **role router** that reads `/me` after sign-in and sends a customer to the family app, a nurse to
> the nurse app, and a brand-new user with no role to role selection. After this phase the app has a real
> front door; f2-b3 (onboarding & profiles) and every authenticated screen build on it.
>
> **Track:** frontend · **Depends on:** [`frontend-phase-0`](./frontend-phase-0.md) (shells, the OTP/phone
> composites, the `services/{domain}` + Query pattern, the cookie manager, `AuthContext`) · **backend
> contract** [`dev/contracts/domains/identity-auth.md`](../../contracts/domains/identity-auth.md) (from
> backend-phase-2) · **Unlocks:** every authenticated screen; f2-b3 onboarding & profiles
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.

---

## 1. Context — where this sits

The client (`client/`, **Next.js 16 App Router + React 19 + MUI v9 + next-intl**, `fa` default & RTL) has
a full app shell, the three actor app shells from f0, and a cookie/fetch/Query/auth stack — but **no way
to log in**. Today's auth domain is a **username/password stub** (`useLogin({ username, password })` →
`/auth/login`) with no UI; per the product docs, Balinyaar's only real credential is **phone-OTP**
([`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md):
"Phone number is the primary login credential … Authentication is phone-OTP"). This phase swaps the stub
for the real thing and adds the role router that turns "authenticated" into "in the right app".

**What already exists (do not rebuild) — confirmed in the codebase:**
- **From [`frontend-phase-0`](./frontend-phase-0.md):** the three actor app shells + role-scoped route
  groups under `(private-routes)`; the **shared OTP code input** and **Iranian phone-number field**
  composites (`src/components/…` with co-located tests); the **`services/{domain}` + TanStack Query**
  reference pattern (a `keys.ts` factory, `apis/clientApi.ts` over `clientFetch`, one-hook-per-file,
  invalidate-on-mutation); the **types-from-contract** convention; the money/format util; the `auth`,
  `common`, `nav` i18n namespaces seeded in **both** `en.json`/`fa.json`. Read the f0 report
  ([`reports/frontend-phase-0-report.md`](../../shared-working-context/reports/frontend-phase-0-report.md))
  for exactly what shipped and how the mock-`clientApi` seam works.
- **The stack (built before f0):** root layout `src/app/[locale]/layout.tsx` (providers NextIntl → Auth →
  Theme → Query → Notistack); `clientFetch`/`serverFetch` + `ApiError` (`@/lib/api`) — **401 already
  clears cookies + toasts + redirects without throwing; 403/5xx toast+throw; other 4xx throw-no-toast**;
  the **cookie manager** (`@/lib/cookies/client` — `setClientCookie`/`deleteClientCookie`, with
  `AUTH_ACCESS_COOKIE_OPTIONS` ≈15 min and `AUTH_REFRESH_COOKIE_OPTIONS` ≈7 d, both **non-httpOnly** by
  the current design); `AuthContext` (`useReducer` `[state, dispatch]`, actions `LOG_IN{user?}`/`LOG_OUT`,
  server-seeded `initialState`); `makeQueryClient` (staleTime 30 s); the **middleware auth gate**
  (`isTokenAlive` on `access_token`, redirects non-public paths to `/${locale}/login`).
- **The auth domain (the stub you are replacing):** `src/services/auth/` — `types.ts`
  (`LoginDto {username,password}`, `AuthTokens {accessToken,refreshToken}`, `User {id,username}`),
  `keys.ts` (`authKeys.currentUser()`), `apis/clientApi.ts` (`AuthClientApi.login/logout/getCurrentUser`),
  `hooks/useLogin|useLogout|useCurrentUser`, `index.ts`. The login hook writes both tokens as non-httpOnly
  cookies and dispatches `LOG_IN`; `useLogout` deletes both, dispatches `LOG_OUT`, redirects to
  `/${locale}/login`. **There is no login page yet** — only the hooks. You extend this domain in place.

**What you are replacing / removing in this phase:**
- The username/password `LoginDto` + `useLogin({username,password})` + `POST /auth/login` path — replaced
  by the OTP request/verify pair. Remove the dead `username/password` types and the `login` api/hook once
  the OTP flow is wired; don't leave both (no-unused-vars is a lint **error**).

> **Wireframe reality check:** A1/A2 (customer) and B1/B2 (nurse) are the **same OTP mechanism** with
> different copy and a different post-login destination. Build **one** OTP flow and parameterise the
> intended-role/copy — do not fork two parallel login stacks.

## 2. Required reading (do this first)

- [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and
  [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md) — how you
  work, the gate, the §6 contract-gap handoff, the caching/re-render rules.
- **The contract you consume:** [`dev/contracts/domains/identity-auth.md`](../../contracts/domains/identity-auth.md)
  (published by **backend-phase-2**) — the **source of truth** for the OTP/refresh/logout/me shapes, routes,
  status codes, and the `Me`/roles enum. **Do not guess shapes.** Read it together with
  [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md)
  (envelope, `snake_case` routes — `[controller]/[action]` so `RequestOtp` → `.../request_otp`; 400/401/403/409
  meanings; the locale header) and
  [`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md)
  (enums cross the wire as **stable string codes** — `male`/`female`, roles as codes — and the frontend
  mirrors them as string-literal unions; **never** hardcode a display label off a code, labels are i18n keys).
  **If the contract isn't published yet or a needed shape is missing** (e.g. the exact `Me` role codes, the
  resend-cooldown / max-attempts fields), follow operating-rules §6: append a request to
  [`dev/shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
  and mock behind the `services/auth` seam meanwhile (§4 below).
- [`client/CLAUDE.md`](../../../client/CLAUDE.md) — RSC/client boundary, the cookie rules (**auth state only
  through the cookie manager — never `document.cookie`/`localStorage`**), the services pattern, anti-patterns.
- **Invoke the `frontend-designer` skill** before any visual work — A1/A2/B1 are full branded screens
  (brand mark, teal `#1d4a40` primary CTA, RTL Persian, Vazirmatn/Mikhak). It is the palette/token/typography
  and `App*`-library contract; all the login UI goes through it.
- [`product/wireframes/index.html`](../../../product/wireframes/index.html) — screens **A1, A2, B1, B2**
  (and the role-router branch implied by A5 home vs B3 nurse status). The exact Persian strings are there:
  A1 CTA **"دریافت کد تایید"**, nurse-switch link **"پرستار هستید؟ ورود پرستاران ←"**; A2 resend
  **"ارسال مجدد کد تا ۰۰:۴۵"** + CTA **"تایید و ادامه"**; B1 subtitle
  **"ویژه پرستاران دارای پروانه نظام پرستاری"**, CTA **"دریافت کد تایید"**; B2 CTA **"تایید و ورود"**.
- [`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md) —
  the auth/role rules: **phone is the primary credential** (email never a login key), customer browses with
  only a verified phone, **sessions are revocable with refresh-token rotation + stolen-token detection**,
  admin roles are **internal-only / never self-assigned**.
- The existing `src/services/auth/*`, `src/context/auth/*`, `src/lib/cookies/*`, `src/lib/api/*`,
  `src/middleware.ts` — the exact code you extend.

## 3. Scope — build this

A vertical slice: **service → hooks → screens → role router**, all over the published contract.

### 3.1 Rewrite `services/auth` for OTP (replace the username/password stub)

Keep the f0 service shape (`types.ts` / `keys.ts` / `apis/clientApi.ts` / `hooks/use*.ts` / `index.ts`);
swap its contents to OTP. **Mirror the published contract for every shape** — the names below are the
intended surface; reconcile field names/casing against `swagger.json` and file a `for-backend.md` request
for any gap.

- **`types.ts`** — replace `LoginDto` with:
  - `OtpRequest` — `{ phone: string; intended_role?: RoleCode }` (the role the user is logging in *as*,
    from the A1/B1 switch — used only for routing/copy; the server decides actual roles).
  - `OtpVerify` — `{ phone: string; code: string }`.
  - `Tokens` — `{ accessToken: string; refreshToken: string }` (rename/keep from `AuthTokens`; match wire casing).
  - `Me` — `{ id: number; phone: string; roles: RoleCode[]; active_role?: RoleCode;
    profile_completed: boolean; nurse_verification_status?: NurseVerificationStatus }` — the role-router
    input. Trim/extend to **exactly** what `GET /me` returns in the contract.
  - `RoleCode` — string-literal union `'customer' | 'nurse' | 'admin'` (codes, not labels).
  - `NurseVerificationStatus` — string-literal union mirroring the contract's enum (e.g.
    `'not_started' | 'in_progress' | 'pending_review' | 'verified' | 'rejected'`); used by the router to
    decide whether a nurse lands on B3 status vs the nurse home.
  - `OtpRequestResult` — whatever `request_otp` returns (e.g. `{ resend_after_seconds: number;
    code_length: number; expires_in_seconds: number }`); drive the **resend countdown** and **OTP box
    count** from this, not from a hardcoded `45`/`4`.
- **`keys.ts`** — `authKeys.me()` (replaces `currentUser()`); keep `authKeys.all`.
- **`apis/clientApi.ts`** — `AuthClientApi` over `clientFetch`:
  - `requestOtp(body: OtpRequest): Promise<OtpRequestResult>` → `POST .../auth/request_otp`
  - `verifyOtp(body: OtpVerify): Promise<Tokens>` → `POST .../auth/verify_otp`
  - `refresh(refreshToken: string): Promise<Tokens>` → `POST .../auth/refresh`
  - `logout(): Promise<void>` → `POST .../auth/logout`
  - `getMe(): Promise<Me>` → `GET .../me`
  (Use the **exact snake_case routes** from the contract; the examples follow the `[controller]/[action]`
  transform.) Remove the old `login` call.
- **`hooks/` (one per file):**
  - `useRequestOtp.ts` — `useMutation` over `requestOtp`. `onSuccess` hands the cooldown/length back to A2.
    **Do not toast 401/403/5xx** (the fetch layer does) — only surface **domain 4xx** (e.g. 429
    rate-limit "try again in N s", invalid-phone 400) as inline field/screen errors.
  - `useVerifyOtp.ts` — `useMutation` over `verifyOtp`. `onSuccess`: **persist tokens via the cookie
    manager** (`setClientCookie` with `AUTH_ACCESS_COOKIE_OPTIONS`/`AUTH_REFRESH_COOKIE_OPTIONS`),
    dispatch **`LOG_IN`**, **`invalidateQueries(authKeys.me())`**, then trigger the role router (3.4).
    Map the contract's wrong-code / expired-code / max-attempts (lockout) failures to inline states.
  - `useLogout.ts` — keep behaviour: call `logout`, `deleteClientCookie` both tokens, dispatch `LOG_OUT`,
    **`invalidateQueries(authKeys.me())`** (or `queryClient.clear()` for auth keys), redirect to
    `/${locale}/login`.
  - `useMe.ts` — `useQuery(authKeys.me(), getMe)` (replaces `useCurrentUser`); `enabled` only when
    authenticated; sensible `staleTime`. This is the role router's data source and the shell's role source.
  - `useRefresh.ts` (DEFERRED token-rotation UI is out of scope, but) — expose `refresh` as a callable so
    the **silent refresh** path can rotate the access token when it expires (see 3.5). If the existing
    fetch layer is to own refresh, document that instead and don't duplicate it; flag the decision in your
    report.
- **`index.ts`** — re-export the new hooks; drop `useLogin`/`useCurrentUser`.

### 3.2 A1 — customer phone login screen

The public login page (the route the middleware redirects to: `/${locale}/login`). Per A1 / the
frontend-designer skill:
- Brand mark + tagline; the **Iranian phone-number field** composite from f0 (placeholder `۰۹۱۲ ۰۰۰ ۰۰۰۰`,
  RTL-safe, validates an Iranian mobile).
- Primary CTA **"دریافت کد تایید"** → `useRequestOtp({ phone, intended_role: 'customer' })`; on success,
  advance to A2 (carry the masked phone + cooldown + code length).
- Secondary link **"پرستار هستید؟ ورود پرستاران ←"** → switches to the **nurse login** variant (B1).
- States: **sending** (CTA spinner/disabled), **invalid/non-Iranian number** (inline), **rate-limit hit**
  ("تا N ثانیه دیگر دوباره تلاش کنید"), **network error** (the fetch layer toasts; keep the field intact).

### 3.3 B1/B2 — nurse login switch (same flow, nurse intent)

Not a separate stack — the **same** login screen with `intended_role: 'nurse'`, nurse copy (B1 subtitle
**"ویژه پرستاران دارای پروانه نظام پرستاری"**, OTP CTA **"تایید و ورود"**), and a link back to family
login. Carry `intended_role` through A1→A2 (B1→B2) so the OTP screen shows the right CTA and the router
knows the entry intent. Implement the variant via a query param or a small client switch on the login
page — **no second route tree**.

### 3.4 A2 — OTP screen + auto-advance

Per A2/B2, using the **shared OTP code input** from f0:
- N-digit boxes (N from `OtpRequestResult.code_length`, default 4 per the wireframe), **masked phone echo**
  ("کد به شماره ۰۹۱۲•••۰۰۰۰ ارسال شد"), **resend countdown** **"ارسال مجدد کد تا ۰۰:۴۵"** driven by
  `resend_after_seconds` (a single timer, cleaned up on unmount — no leaked intervals).
- CTA **"تایید و ادامه"** (customer) / **"تایید و ورود"** (nurse) → `useVerifyOtp`.
- States: **auto-advance** (verify fires automatically when the last box is filled), **wrong code**
  (inline, clear boxes, keep focus), **expired code** (offer resend), **max-attempts lockout** (disable
  input + show the locked message + when it lifts), **resend** (re-calls `useRequestOtp`, restarts the
  countdown, disabled until the countdown hits 0).
- On verify success the hook stores tokens + dispatches `LOG_IN` + invalidates `/me`; then the **role
  router** takes over (next).

### 3.5 The role router (after `/me`)

A small client component/hook (`useRoleRouter` or a `RoleRouter` boundary placed where the shell decides
chrome) that consumes `useMe()` and routes:
- **loading** (`/me` in flight) → the f0 `AppLoading` splash; never flash the wrong shell.
- **roles includes `nurse`** → the **nurse app** home; if `nurse_verification_status` is not `verified`,
  land on **B3 verification status** (built in f5) — for now route to the nurse shell's home/placeholder
  and leave a clear pointer (f5 fills B3). A persistent "verification in progress" banner is **f5's** job;
  here just route correctly.
- **roles includes `customer`** (and not nurse, or `active_role === 'customer'`) → the **customer/family
  app** home (A5, built in f4 — route to the customer shell's home/placeholder now).
- **roles empty / no usable role** → the **SelectRole** screen (3.6).
- **`active_role`** (if the contract returns one) wins when a user has multiple roles; otherwise prefer the
  `intended_role` carried from login, else default customer.
- **admin** → the admin shell (f15) — route correctly but don't build admin screens here.

Honour the **middleware gate**: it already redirects unauthenticated users to `/login`; the role router
runs **after** auth and only decides *which app*. Don't duplicate the auth check in the router.

### 3.6 SelectRole screen (first use)

A minimal post-login screen for a brand-new user with **no role**: choose **خانواده (customer)** or
**پرستار (nurse)** → call the role-selection mutation (`POST /me/role`, `SelectRoleCommand` per the
digest; consume the contract's exact route/body) → `invalidateQueries(authKeys.me())` → re-run the router.
Admin is **never** selectable here (admin roles are internal-only). If `intended_role` from login is set,
pre-select it. Keep this screen shared/simple; the full onboarding (A3 "who is care for", A4 add-patient,
nurse profile bootstrap) is **f2-b3** — link it, don't build it.

### 3.7 Seed `AuthState` with roles

Extend the auth context beyond `{ id, username }`:
- `src/context/auth/types.ts` — extend `AuthState.currentUser` to carry `{ id; phone; roles: RoleCode[];
  active_role?: RoleCode }` (mirror the `Me` essentials the shell needs for chrome) and keep
  `isAuthenticated`. Keep the reducer's `LOG_IN{user?}`/`LOG_OUT` actions; just widen the `user` payload.
- The server-seeded `initialState` (root layout) should seed roles when it can derive them
  (it reads cookies/`getServerAuthState`); if roles aren't available server-side yet, seed
  `isAuthenticated` only and let `useMe()` hydrate roles client-side — **don't** put a second source of
  truth for roles. Document which it is in your report.
- The f0 shells already read "a role" to pick chrome — now feed them the real `roles`/`active_role`.

### 3.8 i18n

All A1/A2/B1/B2/SelectRole/role-router strings as keys in the **`auth`** namespace (and `common`/`nav`
for shared bits) in **both** `messages/en.json` and `messages/fa.json`, in sync, RTL-first. No label is
ever derived from a role/status **code** in code — codes map to i18n keys. (DEFERRED) onboarding copy
(A3/A4) belongs to f2's `onboarding` namespace.

## 4. Mocks & seams in this phase

This phase **introduces no new external-service seam** — OTP/SMS delivery is the **backend's** seam
(`ISmsSender`, introduced in backend-phase-2; the frontend never sends SMS). The only frontend "mock" is
the standard **contract-gap fallback** from operating-rules §6 and the f0 pattern:

- **If [`identity-auth.md`](../../contracts/domains/identity-auth.md) / the b2 swagger snapshot isn't
  merged when you start**, build a **mock `AuthClientApi`** behind the same `services/auth` seam (the f0
  mock-`clientApi` pattern): `requestOtp` returns a fixed `{ resend_after_seconds: 45, code_length: 4,
  expires_in_seconds: 120 }`; `verifyOtp` accepts a fixed dev code (e.g. `0000`) and returns fake tokens
  + a `Me` you can toggle (customer / nurse-unverified / no-role) to exercise all three router branches;
  `getMe` returns the matching `Me`. Selection is by config (env/flag), **never** an `if (mock)` in a
  hook or screen.
- **Record it:** append a row to
  [`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) (seam =
  `AuthClientApi` mock + file, what's faked, the config key, exact swap steps = point the service at the
  real `clientApi` once the contract is live) and a follow-up in your phase report.
- Any shape the contract doesn't cover (resend/lockout fields, `Me` role codes, `active_role`) →
  **append a request** to
  [`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md); do **not** edit
  backend files.

(Reuse the f0 mock-seam mechanism — don't invent a new one.)

## 5. Critical rules you must not get wrong

- **Phone is the only credential.** No username/password anywhere after this phase; email is never a login
  key. Remove the stub `login` path completely (no dead code — it's a lint error).
- **No token in `localStorage`, ever.** Tokens live **only** in cookies via the cookie manager
  (`@/lib/cookies/client`), non-httpOnly per the current design. Never `document.cookie`/`localStorage`
  for auth. Logout must actually clear both cookies *and* the server session (call `logout`).
- **Sessions are revocable; refresh rotates.** Wire `refresh` so an expired access token is silently
  rotated (or confirm the fetch layer owns this and don't duplicate it). The server does reuse-detection;
  the client must send the current refresh token and replace **both** tokens on a successful refresh.
- **`invalidateQueries(authKeys.me())` on login and logout.** `/me` is the role router's input — a stale
  `/me` routes the wrong person to the wrong app. Never read roles from two sources.
- **Don't toast 401/403/5xx in hooks** — the fetch layer already does. Hooks surface only **domain 4xx**
  (invalid phone, rate-limit/429, wrong/expired code, lockout) as inline UI states.
- **OTP states are explicit, not edges:** sending, resend-cooldown, wrong code, expired code,
  **max-attempts lockout**, auto-advance. A single timer, cleaned up on unmount (no leaked intervals →
  no re-render storms).
- **The router never flashes the wrong shell.** Show `AppLoading` while `/me` is in flight; only render a
  shell once the role is known. **admin role is never self-assignable** — SelectRole offers customer/nurse
  only.
- **RSC/client boundary:** login screens, OTP, the router, and the cookie writes are **client**
  (`'use client'`); no `next/headers`/`@/lib/cookies/server` in them. The middleware already gates routes —
  don't re-implement the gate in a component.
- **Both locales, RTL-first, tokens for colour, MUI v9, MUI primitives stay MUI.** Every string in
  `en.json` **and** `fa.json`; no label derived from a code.
- **Minimise re-renders:** the countdown timer and per-box OTP state stay **low** (in the OTP component),
  not in a high context; `useMe` subscribers use `select` if they only need `roles`.

## 6. Definition of Done

The shared [definition-of-done.md](../_shared/definition-of-done.md), plus:
- [ ] `services/auth` is OTP-based: `OtpRequest`/`OtpVerify`/`Tokens`/`Me`(+roles) types, `authKeys.me()`,
      `requestOtp`/`verifyOtp`/`refresh`/`logout`/`getMe` apis, and the `useRequestOtp`/`useVerifyOtp`/
      `useMe`/`useLogout` hooks — the username/password stub is **removed**, not left dangling.
- [ ] A1 (customer login), A2 (OTP, auto-advance, resend countdown, lockout), and the **B1/B2 nurse
      switch** render per the wireframe via the f0 phone/OTP composites, RTL, both locales, branded.
- [ ] The **role router** sends customer→family app, nurse→nurse app (B3 status if unverified),
      no-role→**SelectRole**, admin→admin shell; it shows `AppLoading` during `/me` and never flashes the
      wrong shell.
- [ ] Tokens are stored **only** via the cookie manager; `LOG_IN`/`LOG_OUT` dispatched; `authKeys.me()`
      invalidated on login **and** logout; refresh wired (or fetch-layer ownership documented).
- [ ] `AuthState` carries `roles`/`active_role`; the f0 shells pick chrome from the real role.
- [ ] All strings in `auth`/`common`/`nav` in **both** `en.json` and `fa.json`, in sync.
- [ ] `npm run check` green; `npm run test:ci` green (you touched/added shared composites and the
      router — add tests for the OTP state machine and the router branches).
- [ ] `client/CLAUDE.md` updated: the OTP login flow, the role-router seam, and the widened `AuthState`
      noted in *Project Structure* / the auth notes; the now-stale username/password references corrected.

## 7. How to test (what a human can verify after this phase)

With the b2 backend running (or the mock seam configured), `npm run dev`:
- **Customer happy path:** open `/fa/login` → enter a valid mobile → "دریافت کد تایید" → A2 shows the
  masked phone + a counting-down resend → enter the code (auto-advance fires verify) → tokens appear in
  cookies (DevTools → Application → Cookies; **nothing** in localStorage) → redirected to the **customer**
  app home. `/me` is in the Query cache (React Query Devtools).
- **Nurse switch:** from A1 tap "پرستار هستید؟ ورود پرستاران ←" → B1 nurse copy/subtitle → request →
  verify ("تایید و ورود") → routed to the **nurse** app (B3 status placeholder if unverified).
- **No-role user:** verify as a user whose `/me` has empty `roles` → lands on **SelectRole** → choose a
  role → re-routed into that app.
- **OTP edge states:** wrong code → inline error + boxes clear; let the code expire → resend re-enabled by
  the countdown; trigger max attempts → input locks with a clear message.
- **Session:** log out → both cookies cleared, `/me` invalidated, redirected to `/login`; let the access
  token expire and act → silent refresh rotates it (or you're sent to `/login` cleanly).
- **i18n/RTL:** switch to `en` → all auth strings translate, layout stays correct; `fa` is RTL.
- `npm run check` and `npm run test:ci` pass (OTP state-machine + router-branch tests included).

## 8. Hand off & document (close the phase)

- **Docs:** update [`client/CLAUDE.md`](../../../client/CLAUDE.md) — the phone-OTP flow, the `useRoleRouter`
  seam, the widened `AuthState` (roles/active_role), and the new auth screens/route; **fix the stale
  username/password references**. If you add a route group/folder, update *Project Structure* in the same
  change. No `product/` rule changes expected (you're implementing decided rules); if you discover drift,
  record it there.
- **Contract:** **consume** [`dev/contracts/domains/identity-auth.md`](../../contracts/domains/identity-auth.md)
  — types come from it, not guesses. Append every gap (resend/lockout fields, exact `Me` role codes,
  `active_role`, role-selection route/body) to
  [`dev/shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md).
  Frontend produces **no** contract.
- **Handoff & report:** append to
  [`dev/shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md);
  write [`reports/frontend-phase-1-report.md`](../../shared-working-context/reports/frontend-phase-1-report.md)
  (what was built, **what is now testable and exactly how**, what's mocked behind the `AuthClientApi` seam
  + how f-next swaps it, the contract consumed + gaps filed, follow-ups: B3 banner is f5, A3/A4 onboarding
  is f2, admin screens are f15). Update
  [`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) for the
  `AuthClientApi` mock if you used it.
- **Memory:** save a `project` memory note for the phone-OTP decision, the role-router seam + branch logic,
  and the widened `AuthState`, with a `MEMORY.md` pointer — what a future agent can't cheaply re-derive.
