W30 — Live Verification Gate — Ticket #3269 (Forgot Password Flow, mobile+OTP)
================================================================================

VERDICT: FAILED — deterministic production build failure. The application
cannot be built or served in a usable state once any page that imports this
ticket's own Server Action module is requested; the failure additionally
takes down the pre-existing /login page in the same dev-server process.

--------------------------------------------------------------------------------
1. ENVIRONMENT / SETUP
--------------------------------------------------------------------------------
- Repo: oms-web (Next.js 14.2.35, App Router)
- DATABASE_URL: postgresql://oms_test:***@localhost:5432/oms_test?schema=public
  (real local Postgres, not mocked)
- `npx prisma migrate status` on entry showed 3 unapplied migrations, including
  this ticket's own 20260904000000_password_reset_flow. Applied via
  `npx prisma migrate deploy` (all 3 applied cleanly, `PasswordResetFlow` table
  confirmed present with expected columns/indexes/FK/check-constraint).
- `npx prisma generate` run after migrating (Prisma Client v5.20 -> v5.22 already
  installed; regenerated to pick up the new PasswordResetFlow model).
- Seeded via `SUPER_ADMIN_MOBILE=9998887776 npm run seed` (prisma/seed.ts,
  upsert-based, deterministic). Confirmed row:
    username=superadmin  mobile=9998887776  isActive=t
  (SUPER_ADMIN_USERNAME=superadmin / SUPER_ADMIN_PASSWORD=SuperAdminPw123! from
  the checked-out .env — not invented for this run).

--------------------------------------------------------------------------------
2. BUILD FAILURE — root cause, reproduced 2x via `next build`, 2x via `next dev`
--------------------------------------------------------------------------------
`npm run build` (next build, production mode):

    ./lib/actions/forgot-password.ts
    Error:
      x Only async functions are allowed to be exported in a "use server" file.
        ,-[lib/actions/forgot-password.ts:49:1]
     49 | // Constants (must stay in sync with lib/queries/forgot-password.ts)
     52 | export const RESET_FLOW_COOKIE = "oms_reset_flow";
        `----
    Import trace for requested module:
    ./lib/actions/forgot-password.ts
    ./components/auth/ForgotPasswordForm.tsx
    (second, identical error via ./lib/forgot-password/index.ts ->
     app/(auth)/reset-password/page.tsx)

    > Build failed because of webpack errors

Root cause: `lib/actions/forgot-password.ts` starts with `"use server"` (making
every export in the file a Server Action reference), but line 52 exports a
plain string constant, `RESET_FLOW_COOKIE`. Next.js's "use server" file
convention requires every export to be an async function — this is a hard
compiler-enforced rule, not a lint nit. `lib/forgot-password/index.ts` (the
ticket's own barrel file) re-exports that same constant, and it is imported
by `ForgotPasswordForm.tsx`, `VerifyOtpForm.tsx` (via the barrel), and both
the verify-otp/reset-password Server Component pages (for the cookie name).

Ran `rm -rf .next && npm run build` a second time: identical error, identical
line number, identical trace. Deterministic, not a flake.

--------------------------------------------------------------------------------
3. LIVE SERVER BEHAVIOR (`next dev`, port 3101, fresh `.next`)
--------------------------------------------------------------------------------
Sequence (curl, real HTTP, real dev server):

  GET /login              -> 200   (compiled cleanly, in isolation)
  GET /forgot-password    -> 500   (webpack compile error above, in-band)
  GET /login  (retried)   -> 500   (SAME login page that was 200 seconds ago)
  GET /verify-otp         -> 500   (same compile error)
  GET /reset-password     -> 500   (same compile error)
  GET /forgot-password (again) -> 500 (25ms — cached failed-compilation state)
  GET /api/nonexistent    -> 401   (middleware's own auth gate still runs —
                                     confirms the process itself is alive,
                                     this isn't a crashed server, it's a
                                     poisoned webpack compilation)

Repeated the whole sequence on a second, completely fresh dev-server process
(new port, `rm -rf .next` again): identical results — /login 200 first, then
500 for every route (including /login) the instant /forgot-password is hit.

This means ticket #3269's own diff does not just fail to deliver its own
feature — it also regresses AC-48e7fa93 ("Existing login ... flows continue
to work unchanged"): a real user who visits /login AFTER anyone (or any bot/
crawler) has hit /forgot-password on the same running server gets a 500,
through no fault of their own login attempt. This was confirmed live, not
inferred from the build log alone.

--------------------------------------------------------------------------------
4. STATIC REVIEW OF THE 4 DECLARED SERVER ACTIONS (code-read only — none of
   these could be exercised end-to-end through a live HTTP round trip because
   every page that would surface their form/action reference 500s; see 3, 5)
--------------------------------------------------------------------------------
lib/actions/forgot-password.ts declares 4 Server Actions:
  - requestResetCode  (Step 1 — send OTP)
  - resendResetCode   (Step 1b — resend, 60s cooldown via lastSentAt)
  - verifyResetCode   (Step 2 — check OTP, 5-attempt lockout -> row delete)
  - completeReset     (Step 3 — set new password, revoke sessions, consume flow)

None of the 4 are permission-gated (by design — this is the unauthenticated
recovery path; middleware.ts's matcher explicitly excludes these routes from
the session gate). permission_gated = 0.

Logic read (OTP hashing/expiry/reuse/history/session-revocation) looks
correctly implemented on paper — SHA-256 hash storage, timing-safe compare,
expiresAt/consumedAt/verifiedAt state machine, MAX_ATTEMPTS=5 hard-delete,
PasswordHistory last-3 reuse check, full session revocation on reset. This
could not be confirmed by actually running the flow since no page hosting
these forms will render.

--------------------------------------------------------------------------------
5. ACCEPTANCE CRITERIA — additional FAILs found by static/code inspection
   (independent of the build failure, and would still fail even if the build
   were fixed)
--------------------------------------------------------------------------------
- AC-c863f09c / AC-9f181d54 (Sign In "Forgot Password?" link + navigation):
  FAILED. `components/auth/SignInForm.tsx` (the Sign In screen's actual form)
  has no "Forgot Password" link or any reference to /forgot-password at all.
  `grep -rn "Forgot Password|forgot-password" app` only matches the
  forgot-password/verify-otp/reset-password pages themselves — nothing in
  app/(auth)/login/page.tsx or SignInForm.tsx. There is no way to reach this
  flow from the Sign In screen as built.

- AC-856f6630 (six individual digit boxes): FAILED. `VerifyOtpForm.tsx`
  renders ONE `<input maxLength={6}>` text field (letter-spaced via CSS
  `tracking-[0.5em]` to visually approximate six boxes), not six discrete
  input elements. The ticket's own requirement text explicitly says "six
  individual digit boxes, matching the design."

- AC-fe7ba705 (Password Reset Success screen/state): FAILED. `completeReset`
  ends with a bare server-side `redirect("/login")` — no query param, no
  flash/toast state, and `app/(auth)/login/page.tsx` / `SignInForm.tsx` read
  no search param and render no success message. A user is silently dropped
  back at a blank Sign In form with zero confirmation the reset worked.

- AC-fc35922f (rate limiting on request AND resend endpoints): PARTIAL FAIL.
  `resendResetCode` has a real 60s cooldown (`RESEND_COOLDOWN_MS`, enforced
  server-side via `lastSentAt`). `requestResetCode` (the initial "Send Reset
  Code" submission) has no rate limiting or cooldown at all — grepped the
  whole repo for rate-limit-related identifiers, only match is inside
  forgot-password.ts itself (the resend cooldown). A caller can invoke
  requestResetCode unlimited times per minute for the same mobile number,
  each call doing a delete+insert+(no-op SMS stub) with no throttle.

These are independent, additional findings layered on top of the build
failure in section 2 — even a hypothetical fixed build would still fail
AC-c863f09c, AC-9f181d54, AC-856f6630, AC-fe7ba705, and partially AC-fc35922f.

--------------------------------------------------------------------------------
6. WHAT COULD NOT BE VERIFIED
--------------------------------------------------------------------------------
Every AC that requires actually submitting a form and observing a live
response (valid/invalid/expired/reused OTP, resend timer live countdown,
password policy enforcement round-trip, password mismatch inline error,
successful reset, login with the new password, unregistered mobile handling,
network-failure UI resilience, refresh/back-navigation mid-flow) could NOT be
exercised — every page in this flow, and (transitively, once touched) /login
itself, returns HTTP 500 from the real Next.js compiler, before any React
tree or form ever renders. This is not a scope limitation of this verification
run; it is a direct, structural consequence of the build failure in section 2.

--------------------------------------------------------------------------------
7. RETRIES
--------------------------------------------------------------------------------
- `next build`: run twice (fresh `.next` both times) — identical error,
  identical file/line, both times.
- `next dev`: run as two separate fresh server processes (different ports,
  fresh `.next` both times) — identical 500 cascade, including /login
  breaking after /forgot-password was hit, both times.
Classification: deterministic (not flake_suspected). Same root cause, same
file, same line, 4/4 runs.
