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 deps = createDeps({ members: [alice, carol] });
|
||||||
const handle = createMessagingInboundHandler(deps);
|
const handle = createMessagingInboundHandler(deps);
|
||||||
await handle(groupEvent);
|
await handle({ ...groupEvent, participantsAuthoritative: true });
|
||||||
|
|
||||||
expect(deps.prisma.messagingChannelMember.updateMany).toHaveBeenCalledWith(
|
expect(deps.prisma.messagingChannelMember.updateMany).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1004,6 +1004,35 @@ describe("createMessagingInboundHandler channel routing", () => {
|
|||||||
expect(sparse.prisma.messagingChannelMember.updateMany).not.toHaveBeenCalled();
|
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 () => {
|
it("sanitizes attacker-controlled channel names before storing them", async () => {
|
||||||
const deps = createDeps();
|
const deps = createDeps();
|
||||||
const handle = createMessagingInboundHandler(deps);
|
const handle = createMessagingInboundHandler(deps);
|
||||||
|
|||||||
@ -537,7 +537,7 @@ async function handleChannelEvent(
|
|||||||
// Someone removed from the group must stop receiving its content.
|
// Someone removed from the group must stop receiving its content.
|
||||||
// A webhook without a participants roster says nothing about membership —
|
// A webhook without a participants roster says nothing about membership —
|
||||||
// never sweep on partial data.
|
// never sweep on partial data.
|
||||||
if (event.participants.length > 0) {
|
if (event.participantsAuthoritative) {
|
||||||
await deps.prisma.messagingChannelMember.updateMany({
|
await deps.prisma.messagingChannelMember.updateMany({
|
||||||
where: {
|
where: {
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
|
|||||||
@ -587,6 +587,11 @@ export interface MessagingInboundMessage {
|
|||||||
channelName: string | null;
|
channelName: string | null;
|
||||||
/** Group roster addresses when the platform reports them; often empty. */
|
/** Group roster addresses when the platform reports them; often empty. */
|
||||||
participants: string[];
|
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;
|
content: string;
|
||||||
mediaUrl: string | null;
|
mediaUrl: string | null;
|
||||||
/** Team-room workspace/team id when the platform reports one (Slack team_id, …). */
|
/** Team-room workspace/team id when the platform reports one (Slack team_id, …). */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user