Serve the widened accept gate, add self-assignment, and close two contact-mask bypasses

FeatureSecurityService
Shipped
September 3, 2026 at 8:02 PM UTC
Author
Kamo
Commit
f35500c

**Endpoints for the new gate.** /leads/available-summary and /leads/available now report both reasons a member is blocked — leads owed a note since they were assigned, and leads still at their intake status — with the leads themselves and the SYSTEM status wording plus ids, so the browser can resolve the org's own custom labels before showing anybody the reason they are stuck. blockedByComments is kept as an alias of the new `prohibited` so a browser still running the previous bundle keeps working through the deploy. The gate is evaluated for anyone who can see leads, not only members holding SPEND_RECEIVE_LEAD_CREDIT: a member who can only claim a free-for-all lead is subject to the same discipline and their nav has to be able to say so. The credit-pool scan below it still requires the credit right. **POST /leads/{id}/assign-to-me** claims one unassigned lead. It returns the lead UNMASKED — the caller is its assignee by the time the server answers — which is what lets the detail page reveal the contact information without a reload. **A new NATS subject, **************** published when a note lands on a lead, when a status changes, and on both sides of every assignment change. MediaService's relay already subscribes to security.leads.> and maps subjects to topics generically, so it needs no change. The payload carries only a member id: the gate's answer depends on that member's whole book, so there is nothing useful to put on an org-wide topic, and a subject every member receives is the wrong place for anything worth withholding. **Two contact-mask bypasses, both found auditing where a lead's details can reach a browser:** - GET /leads/{id}/history had NO assignment or contact gate at all. A field-change log is a complete record of every value a field has ever held, so it returned the e-mail and phone numbers the lead's own mask takes out — and not just the current ones, every previous one too. It now carries the same two decisions as getLeadById, and LeadHistoryContactMask obfuscates the contact-bearing rows. Status, assignment and loan-amount history are untouched: this withholds contact information, not the audit trail. - GET /leads/callback-context/{id} shipped an e-mail address and two phone numbers for any callback in the org, ungated. A callback id was a way to read the contact details of any colleague's lead. **A write-back guard on updateLead.** The read mask no longer blanks contact fields, it obfuscates them — so a masked caller now holds real-looking strings that would persist perfectly happily. The right hierarchy makes this hard to reach, but "hard to reach" is not a guarantee and the failure mode is silent corruption of a customer's phone number. **Grid visibility.** Without VIEW_UNASSIGNED_LEADS the /leads floor becomes "my leads plus unassigned free-for-all leads" rather than "my leads", and an unassigned free-for-all lead can be opened and its history read — a row you can see but cannot open is a bug. Reading the CORRESPONDENCE on one is a different question and LeadCommsReadGate still answers it with VIEW_LEAD_CONTACT_INFO_OTHERS; the grid-comms scope deliberately keeps the narrower floor, with a comment saying why the two now differ.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing