12 KiB
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 <commit> 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.tsis correct as written (PUBLIC_PATHS =['/login'], redirect on dead token) but never runs: live cookie-lessGET /fa/admin→ 200 full HTML, bareGET /→ 404 (no next-intl locale redirect either) — reproduced under Turbopack and--webpack. Cause: Next.js infers the workspace root from a strayC:\Users\Lenovo\pnpm-lock.yaml(startup warning names it), so the middleware file is never bound. Also: Next 16 deprecatesmiddleware.tsforproxy.ts, and the docs place the file "at the same level aspagesorapp" — for this repo that'sclient/src/. Fix: pinturbopack.root(+outputFileTracingRoot) inclient/next.config.mjs; migrate the file tosrc/proxy.ts; prove the gate runs with a runtime probe (curl DoD). -
H-02 (blocker) —
isTokenAlivecan 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-38JSON-parses segment 1 ⇒ alwaysnull⇒isTokenAlivealwaysfalsefor 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) seedsisAuthenticated=falseon every hard reload. The wire already returnsaccessExpiresAt/refreshExpiresAt(client/src/services/auth/types.ts:62-66): persist a readable companion expiry cookie inpersistAuthTokensand check that instead of decoding. -
H-03 (high) — anonymous visitor to a private shell = infinite splash, never a login ask.
useMe()is enabled only whenisAuthenticated; for a cookie-less session that's permanentlyfalse, souseRoleHydration()never leavesloadingandRoleGuard(client/src/components/auth/RoleGuard.tsx:55) rendersAuthSplashforever (splash markup confirmed in served HTML). Add an explicitunauthenticatedstate → 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:9passes only the literal role"admin"; the seeded personas holdsuper_admin/finance(DemoWorldDefinitions.cs:144-145, distinct constantsRoleNames.cs:13,17). No RoleClaim is ever seeded, and the only claim writer (RoleManagerService.ChangeRolePermissionsAsync,RoleManagerService.cs:145-208) has no HTTP surface (theFeatures/{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.Adminover-grants: Support/Moderation get clinical + financial access.Features/Bookings/BookingRoles.cs:8-9bundles Admin/SuperAdmin/Support/Finance/Moderation; used byGetCareInstructionsQuery.Handler.cs:31,37(encrypted clinical notes),TransitionBookingStatusCommand.Handler.cs:24-25,51-54(forceCompleted⇒ 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 intoClinicalAccess/Financialsets 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,88importsmockGetBookingForRefundfromservices/bookings/apis/mockApi.tswhose fixture array (ids ~5001-5005,:340-345throws 404 otherwise) is orphaned sinceUSE_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-224pushes the realrequestId(payment+bookingRequests are real) into the BNPL branch;services/bnpl/apis/mockApi.ts:5-8hard-importsbookingRequestsMockApi+mockInsertConvertedBooking(raw mock modules, bypassing both domains' seams); store holds only fixture ids 1-2 → 404, or on id collisionsettle()(: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 whileUSE_BNPL_MOCK && !USE_BOOKING_REQUESTS_MOCK; any surviving mock must resolve requests through the realbookingRequestsApiselector. -
H-08 (blocker) — verification is 100% mocked while catalog/search are real → invisible nurses.
services/verification/constants.ts:8-9USE_VERIFICATION_MOCK=truegates the WHOLEVerificationApi(nurse flow + TrustBadge + admin queue, one interface —types.ts:255-265). The mock's__mockApproveAllnever flips the realnurse_profiles.is_verified(NurseProfile.cs:44-53, private setter, only the real b6VerificationAggregatorcalls it), but variant activation is real and unconditional (SetVariantActiveCommand.Handler.cs:15-36), and the search gate requiresIsVerified(SearchIndexMaintainer.cs:34,65,94,152).PublishGate.tsx:59-68CTA 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:13USE_PAYOUTS_MOCK=true, but REQ-025 was delivered —NursePayoutsController.cs:33-48serves earnings_balance/earnings/{id}, andpayouts/apis/clientApi.ts:163-187already 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-75types the response asOmit<…,'bookingId'>and setsbookingId: null(line 72) althoughBookingRequestDto.cs:46-49carries 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-30removes onlyauthKeys.all(['auth']);useVerifyOtp.ts:25-32/useSelectRole.ts:19-38touch only auth keys;queryClient.ts:17-27is 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-58renders hardcoded-Englisherror.toString()+ full component stack, no retry, TODO-only reporting; used atCustomerLayout.tsx:78andTopBarAndSideBarLayout.tsx:104— and sits BELOW RoleGuard/providers, while noerror.tsxorglobal-error.tsxexists anywhere undersrc/app. Replace with a branded i18n recovery card (mirrorAuthAccountError), 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— fourdispatchToast('…English…')literals pass throughdispatchToast.ts/ToastBridge.tsxverbatim; 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,121fetch/render with nouseAdminCapabilities()check — only the sidebar (AdminLayout.tsx:24-33) hides them. (Server enforcement exists but is the dead DynamicPermission policy — H-04.) Add aCapabilityGuardearly-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 + canonicalcancellation_policy_codeset, 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_accesscheck; the GET/PUT care_record endpoints already exist —PatientCareRecordsController.cs:44-54) · REQ-029/030 admin configupdatedAt/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:
administersPartnerCenterIdonMeResult+ a seeded demo partner admin; REQ-032/033:centers/me[/nurses|/bookings|/settlement]split reads + invoicetotalIrr.Meinterface (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 toresolveRoleDestinationoff the new/mesignal; gate the portal at the layout level (todaypartner/layout.tsx:15RoleGuard has noexpected, the mock resolves a hardcoded center for ANY caller —partnerCenter/apis/mockApi.ts:237-243— andnurses/bookingspages don't even awaituseMyPartnerCenter). -
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 wirelong Id(CarePlanDtos.cs:11-15) — a naive flip 400s every family-record save; new items should omit id (server assigns). Then flip every remainingUSE_*_MOCK(refunds, bnpl, payouts-admin, admin, partnerCenter, verification-admin, patientRecords) as its REQs land; delete the orphaned cross-mock helpers; updatemocks-registry.md+client/CLAUDE.md.