fix(api): skip group membership sweep on partial participant rosters
Group inbound webhooks often report only the sender in participants, not the full roster. Sweeping on any non-empty list incorrectly marked approved members as left, silently dropping them from channel fan-out. Gate the sweep on participantsAuthoritative so adapters opt in only when the provider supplies a complete roster. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
c8b06ec7a8
commit
18934e03c9
@ -939,7 +939,7 @@ describe("createMessagingInboundHandler channel routing", () => {
|
||||
};
|
||||
const deps = createDeps({ members: [alice, carol] });
|
||||
const handle = createMessagingInboundHandler(deps);
|
||||
await handle(groupEvent);
|
||||
await handle({ ...groupEvent, participantsAuthoritative: true });
|
||||
|
||||
expect(deps.prisma.messagingChannelMember.updateMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -1004,6 +1004,35 @@ describe("createMessagingInboundHandler channel routing", () => {
|
||||
expect(sparse.prisma.messagingChannelMember.updateMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not sweep approved members when the webhook roster is partial", async () => {
|
||||
const alice = {
|
||||
id: "cm-1",
|
||||
channelId: "ch-1",
|
||||
address: "+15551111111",
|
||||
identityId: "mi-1",
|
||||
status: "approved",
|
||||
};
|
||||
const bob = {
|
||||
id: "cm-2",
|
||||
channelId: "ch-1",
|
||||
address: "+15552222222",
|
||||
identityId: "mi-2",
|
||||
status: "approved",
|
||||
};
|
||||
const deps = createDeps({ members: [alice, bob] });
|
||||
const handle = createMessagingInboundHandler(deps);
|
||||
// Sendblue often reports only the sender — not a complete roster.
|
||||
await handle({
|
||||
...groupEvent,
|
||||
participants: ["+15551111111"],
|
||||
});
|
||||
|
||||
expect(deps.prisma.messagingChannelMember.updateMany).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: { status: "left" } }),
|
||||
);
|
||||
expect(bob.status).toBe("approved");
|
||||
});
|
||||
|
||||
it("sanitizes attacker-controlled channel names before storing them", async () => {
|
||||
const deps = createDeps();
|
||||
const handle = createMessagingInboundHandler(deps);
|
||||
|
||||
@ -537,7 +537,7 @@ async function handleChannelEvent(
|
||||
// Someone removed from the group must stop receiving its content.
|
||||
// A webhook without a participants roster says nothing about membership —
|
||||
// never sweep on partial data.
|
||||
if (event.participants.length > 0) {
|
||||
if (event.participantsAuthoritative) {
|
||||
await deps.prisma.messagingChannelMember.updateMany({
|
||||
where: {
|
||||
channelId: channel.id,
|
||||
|
||||
@ -587,6 +587,11 @@ export interface MessagingInboundMessage {
|
||||
channelName: string | null;
|
||||
/** Group roster addresses when the platform reports them; often empty. */
|
||||
participants: string[];
|
||||
/**
|
||||
* True only when {@link participants} is a complete group roster from the
|
||||
* provider. Partial rosters must not trigger membership sweeps.
|
||||
*/
|
||||
participantsAuthoritative?: boolean;
|
||||
content: string;
|
||||
mediaUrl: string | null;
|
||||
/** Team-room workspace/team id when the platform reports one (Slack team_id, …). */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user