One rule for what a member may do with another, served per member and per roster row

FeatureSecurityService
Shipped
September 18, 2026 at 2:35 AM UTC
Author
Kamo
Commit
5063eaa

The same five actions — chat, call, email, meet, view profile — were offered from three places in kamo-internal (the Interaction Center row, the chat hex-head popover, the lead-lock contact card), each deciding in the browser from its own reading of the rights. No two agreed, none could see the chat policy or anything about the person on the other end, and the result was buttons that failed on click. MemberInteractionPolicy (members/card) is now the one rule, pure and table-tested. Per action it answers HIDDEN when the viewer can never do it (no seat, the org is not entitled to the app) or REFUSED with a reason code when it is this person in the way (the chat policy — with the org's own wording — INACTIVE, NO_EMAIL, NO_NUMBER, NO_MOBILE, NOT_PLAYER): chat not restricted; target active colleague; chat policy lets you start it (OVERRIDE_CHAT_POLICY passes) call seat canCall; any extension or number on file text seat canSendTextMessages; a mobile that takes texts email EMAIL entitled, ACCESS_MAIL, seat canSendEmail; an address on file meet/schedule MEET entitled, seat canMeet, START_MEETING; an active colleague profile the member page's own six rights or god — for a customer-side member, VIEW_ACCOUNTS ("View account") game the games both sides can play: entitlement + PLAY_CHESS/PLAY_HOLDEM on each side (owner stands in) A customer-side member's contact details are only for a viewer who may read accounts. Apps are the ENTITLED projection (computeEffectiveFeatures), the same one the browser's organization.isEmail reads. MemberInteractionService resolves the viewer once — seat capabilities from EntitlementService, apps, rights, owner — and decides for one member or a whole roster (game rights read once per game). GET **************** identity (name, avatar, title, department, kind, timezone, availability), the contact values the policy lets this viewer see (a colleague's are the ones the roster already serves every member), the account for a customer-side member, and the decision. Another org's member is 404, like one that does not exist; the id is a path string. The session comes through CallerSessionResolver, so the unguarded-endpoint ratchet recognises it. /by-department rows gain `interaction` from the same policy, so a roster row and a card opened from it can never offer different things. LeadWebinarEntry gains presenterMemberId, so the lead page can put the card on the presenter's name. Tests: MemberInteractionPolicyTest (17), MemberCardEndpointTest (wire shape, contact withheld, 404 across orgs, 400/401), MemberRosterInteractionTest. The five full-suite failures **************** x4, the ratchet naming **************** are the same five that fail on origin/main without this change.

All changes

Like what you see shipping?

All of it arrives in your workspace on its own. Start on the free plan and read this page again in a month.

Start Free ForeverView Pricing