- Shipped
- August 4, 2026 at 4:17 AM UTC
- Author
- kamo
- Commit
- 1fea0c6
SecurityService now withholds custom-form answers from a caller who is not the lead's assignee (and, in a handlesPhi tenant, even from a holder of right 161). That mask is the enforcement; this is not. Two paths still bypass it: - LeadRealtimePublisher puts a FULL, unmasked LeadDTO on security.leads.rt.{orgId} — one payload fanned out to every subscriber in the org, with no per-recipient projection possible. Until that event carries identifiers only and the client refetches, app/lib/leads/leadFieldMask.ts is the only thing between a colleague's intake answers and every open lead pane in the tenant. LeadViewClient now runs incoming realtime payloads through it before any of it reaches state. - Rendering: a withheld form and a never-filled-in one arrive looking identical, so the decision has to be recomputed, not inferred. The custom-form pane drops its Edit affordance when answers are restricted — saving a blank form would PUT {} over the real answers, which the server now 403s anyway. Organization.handlesPhi is delivered on the existing column -> DTO -> OrgContext path (the org entity already serialises it; the zod schema and Organization.fromJSON allow-list were the missing links), not a second channel. No new user-visible copy: the explanatory "restricted" label needs a key in the translation-dictionary repo and is deliberately left for that change.