# Hardening issue ledger (verified findings) Every item below was found by one audit agent and **re-confirmed by an independent adversarial verifier** reading the code (and, for H-01, probing the running dev server). Evidence is file:line at the time of the audit (2026-07-16) — re-locate if lines drifted, but do not re-litigate the finding. **This file is the loop's progress state.** When a phase fixes an item, tick it `[x]` and append `— fixed in ` on the same line. `LOOP-PROMPT.md` reads this file to decide what's left. --- ## Phase 0 — auth gate & session liveness - [ ] **H-01 (blocker) — the auth gate never executes at runtime.** `client/middleware.ts` is correct as written (PUBLIC_PATHS = `['/login']`, redirect on dead token) but never runs: live cookie-less `GET /fa/admin` → 200 full HTML, bare `GET /` → 404 (no next-intl locale redirect either) — reproduced under Turbopack **and** `--webpack`. Cause: Next.js infers the workspace root from a stray `C:\Users\Lenovo\pnpm-lock.yaml` (startup warning names it), so the middleware file is never bound. Also: Next 16 deprecates `middleware.ts` for `proxy.ts`, and the docs place the file "at the same level as `pages` or `app`" — for this repo that's `client/src/`. Fix: pin `turbopack.root` (+ `outputFileTracingRoot`) in `client/next.config.mjs`; migrate the file to `src/proxy.ts`; **prove the gate runs with a runtime probe** (curl DoD). - [ ] **H-02 (blocker) — `isTokenAlive` can never read the real token (JWE).** Server: `server/src/Infrastructure/Baya.Infrastructure.Identity/Jwt/JwtService.cs:116-128` — `EncryptingCredentials(Aes128KW, Aes128CbcHmacSha256)` ⇒ the access token is a 5-segment JWE. Client: `client/src/lib/auth/token.ts:24-38` JSON-parses segment 1 ⇒ always `null` ⇒ `isTokenAlive` always `false` for real tokens. Consequences: (a) once H-01 is fixed the middleware would redirect-loop **logged-in** users; (b) `getServerAuthState` (`client/src/lib/auth/server.ts`) seeds `isAuthenticated=false` on every hard reload. The wire already returns `accessExpiresAt`/`refreshExpiresAt` (`client/src/services/auth/types.ts:62-66`): persist a readable companion expiry cookie in `persistAuthTokens` and check that instead of decoding. - [ ] **H-03 (high) — anonymous visitor to a private shell = infinite splash, never a login ask.** `useMe()` is enabled only when `isAuthenticated`; for a cookie-less session that's permanently `false`, so `useRoleHydration()` never leaves `loading` and `RoleGuard` (`client/src/components/auth/RoleGuard.tsx:55`) renders `AuthSplash` forever (splash markup confirmed in served HTML). Add an explicit `unauthenticated` state → redirect to `/login?next=…` with return-URL honored post-login. ## Phase 1 — admin RBAC & staff role scopes (server) - [ ] **H-04 (blocker) — DynamicPermission RBAC is structurally dead; the whole admin API 403s.** `DynamicPermissionService.cs:9` passes only the literal role `"admin"`; the seeded personas hold `super_admin`/`finance` (`DemoWorldDefinitions.cs:144-145`, distinct constants `RoleNames.cs:13,17`). No RoleClaim is ever seeded, and the only claim writer (`RoleManagerService.ChangeRolePermissionsAsync`, `RoleManagerService.cs:145-208`) has **no HTTP surface** (the `Features/{Role,Admin}` handlers exist but no controller exposes them). Every `[Authorize(ConstantPolicies.DynamicPermission)]` endpoint (all Admin* controllers, PlatformConfig, Holidays, Audit, SupportAlerts, Reviews-moderate) 403s for both seeded admins. RUNBOOK.md:115-130 and refinement-phase-2-report falsely document these personas as working — fix the docs too. - [ ] **H-05 (high) — `BookingRoles.Admin` over-grants: Support/Moderation get clinical + financial access.** `Features/Bookings/BookingRoles.cs:8-9` bundles Admin/SuperAdmin/Support/Finance/Moderation; used by `GetCareInstructionsQuery.Handler.cs:31,37` (encrypted clinical notes), `TransitionBookingStatusCommand.Handler.cs:24-25,51-54` (force `Completed` ⇒ arms the payout dispute-window), `GetNursePayableBalanceQuery.Handler.cs:22-24` (contradicts its own comment). Controllers carry only blanket `[Authorize]` — the handler set IS the boundary. The codebase's own convention is narrower (`PatientAccess.cs:17` = [Admin, SuperAdmin]; `StaffRoles.cs:5-13` "narrowest fitting scope"). Split into `ClinicalAccess` / `Financial` sets and update the handlers. ## Phase 2 — money-path mock integrity (client) - [ ] **H-06 (blocker) — refunds mock reads the retired bookings-mock store → real cancel 404s.** `services/refunds/apis/mockApi.ts:3,88` imports `mockGetBookingForRefund` from `services/bookings/apis/mockApi.ts` whose fixture array (ids ~5001-5005, `:340-345` throws 404 otherwise) is orphaned since `USE_BOOKINGS_MOCK=false`. Cancel CTA renders for every real booking (`bookings/[id]/page.tsx:79-104`) then the preview errors (`bookings/[id]/cancel/page.tsx:50-78`). - [ ] **H-07 (blocker) — BNPL wizard runs on a disconnected mock store keyed by the real request id.** `checkout/page.tsx:213-224` pushes the **real** `requestId` (payment+bookingRequests are real) into the BNPL branch; `services/bnpl/apis/mockApi.ts:5-8` hard-imports `bookingRequestsMockApi` + `mockInsertConvertedBooking` (raw mock modules, bypassing both domains' seams); store holds only fixture ids 1-2 → 404, or on id collision `settle()` (`:260-317`) fabricates a client-side "booking" while the real request expires unpaid. `BNPL_ENABLED=true` (`services/payment/constants.ts:23`) has no gating. Stopgap: hide/derive the CTA while `USE_BNPL_MOCK && !USE_BOOKING_REQUESTS_MOCK`; any surviving mock must resolve requests through the real `bookingRequestsApi` selector. - [ ] **H-08 (blocker) — verification is 100% mocked while catalog/search are real → invisible nurses.** `services/verification/constants.ts:8-9` `USE_VERIFICATION_MOCK=true` gates the WHOLE `VerificationApi` (nurse flow + TrustBadge + admin queue, one interface — `types.ts:255-265`). The mock's `__mockApproveAll` never flips the real `nurse_profiles.is_verified` (`NurseProfile.cs:44-53`, private setter, only the real b6 `VerificationAggregator` calls it), but variant activation is real and unconditional (`SetVariantActiveCommand.Handler.cs:15-36`), and the search gate requires `IsVerified` (`SearchIndexMaintainer.cs:34,65,94,152`). `PublishGate.tsx:59-68` CTA makes no API call at all. Fix: split the seam (nurse half real now — clientApi is 1:1 per the registry; admin half stays mocked until REQ-034) and wire/gate the publish UI honestly. - [ ] **H-09 (high) — nurse earnings fabricated although its endpoints are live.** `services/payouts/constants.ts:13` `USE_PAYOUTS_MOCK=true`, but REQ-025 was **delivered** — `NursePayoutsController.cs:33-48` serves earnings_balance/earnings/{id}, and `payouts/apis/clientApi.ts:163-187` already implements them. The flag is held hostage by the admin batch methods (REQ-036, still open). Split the seam per-method (nurse reads real, admin mock). - [ ] **H-10 (medium) — payment outcome discards the delivered `bookingId`.** `services/payment/apis/clientApi.ts:55-75` types the response as `Omit<…,'bookingId'>` and sets `bookingId: null` (line 72) although `BookingRequestDto.cs:46-49` carries it (REQ-017 delivered). Kills the confirmation deep links (`checkout/return/page.tsx:84`, `confirmation/page.tsx:94-110`) and the targeted cache invalidation (`invalidations.ts:17-24`). Read it off the wire; drop the stale REQ-017-pending comments. ## Phase 3 — session & error-surface hardening (client) - [ ] **H-11 (high) — logout/login never clear the query cache → cross-account data leak.** `useLogout.ts:22-30` removes only `authKeys.all` (`['auth']`); `useVerifyOtp.ts:25-32` / `useSelectRole.ts:19-38` touch only auth keys; `queryClient.ts:17-27` is a tab-lifetime singleton; domain keys carry no user id. User B logging in after User A on the same device is served A's cached patients/bookings/addresses/tickets until staleTime lapses. Fix: `queryClient.clear()` on logout + reset on login. - [ ] **H-12 (high) — raw stack-trace error boundary; no route-level error pages.** `components/common/ErrorBoundary.tsx:35-58` renders hardcoded-English `error.toString()` + full component stack, no retry, TODO-only reporting; used at `CustomerLayout.tsx:78` and `TopBarAndSideBarLayout.tsx:104` — and sits BELOW RoleGuard/providers, while **no `error.tsx` or `global-error.tsx` exists anywhere** under `src/app`. Replace with a branded i18n recovery card (mirror `AuthAccountError`), dev-only stack, and add `[locale]` route-level error files. - [ ] **H-13 (high) — 401/403/5xx/network toasts are hardcoded English.** `lib/api/client.ts:57,80,86,91` — four `dispatchToast('…English…')` literals pass through `dispatchToast.ts`/`ToastBridge.tsx` verbatim; no keys exist in either messages file. Violates the project's golden rule #3 on the fa-default app. Localize via a locale-keyed dictionary (non-React call site — the fetch layer already computes the locale). - [ ] **H-14 (medium) — admin read-only consoles unguarded client-side.** `admin/audit/page.tsx:25`, `admin/verification/page.tsx:50`, `admin/tickets/page.tsx:43`, `admin/roles/page.tsx:50,121` fetch/render with no `useAdminCapabilities()` check — only the sidebar (`AdminLayout.tsx:24-33`) hides them. (Server enforcement exists but is the dead DynamicPermission policy — H-04.) Add a `CapabilityGuard` early-return with the non-leaking access-denied pattern. ## Phase 4 — contract completion batch (server; Tier B/C REQs) - [ ] **H-15 (medium) — the deferred REQ set, per the ledger** (`for-backend.md` — re-check each status before building; REQ-031's HTTP surface may already exist after Phase 1): REQ-019/020/021 customer refunds (cancel command, policy preview + canonical `cancellation_policy_code` set, refund-by-booking + decomposition) · REQ-022/023/024 BNPL (options/schedule/wallet-installments, eligibility KYC fields) · REQ-026 reviews (eligibility/my-review reads — verify what phase 3/4 already delivered) · REQ-027 leftovers (`record_access` check; the GET/PUT care_record endpoints already exist — `PatientCareRecordsController.cs:44-54`) · REQ-029/030 admin config `updatedAt`/`updatedBy` + audit filters · REQ-034 admin verification queue/signed-doc-URL/approve · REQ-035 admin refund preview/initiate/approve/reject · REQ-036 admin payout preview/run/retry/record-reference. - [ ] **H-16a (medium, server half) — partner portal has no identity signal or self-scoped reads.** REQ-038: `administersPartnerCenterId` on `MeResult` + a seeded demo partner admin; REQ-032/033: `centers/me[/nurses|/bookings|/settlement]` split reads + invoice `totalIrr`. `Me` interface (`services/auth/types.ts:79-91`) has no partner field; `resolveRoleDestination` (`services/auth/routing.ts:32-47`) has no partner branch. ## Phase 5 — final de-mock & partner reachability (client) - [ ] **H-16b (medium, client half) — partner routing + portal gate + flip `USE_PARTNER_MOCK`.** Add the partner branch to `resolveRoleDestination` off the new `/me` signal; gate the portal at the layout level (today `partner/layout.tsx:15` RoleGuard has no `expected`, the mock resolves a hardcoded center for ANY caller — `partnerCenter/apis/mockApi.ts:237-243` — and `nurses`/`bookings` pages don't even await `useMyPartnerCenter`). - [ ] **H-17 (medium) — patientRecords id-type mismatch; flip the remaining flags.** Client `Medication/RoutineItem/CareTask.id: string` (`patientRecords/types.ts:36,45,53`, mock seeds 'm1'/'r1'/'t1') vs wire `long Id` (`CarePlanDtos.cs:11-15`) — a naive flip 400s every family-record save; new items should omit id (server assigns). Then flip every remaining `USE_*_MOCK` (refunds, bnpl, payouts-admin, admin, partnerCenter, verification-admin, patientRecords) as its REQs land; delete the orphaned cross-mock helpers; update `mocks-registry.md` + `client/CLAUDE.md`.