fix(dm): enforce read-only guard on DM reactions (WS) + harden purge/ownership tests
Finding 1: handleReactionAdd/Remove now drop reactions on a dead 1-on-1 (isDeadOneOnOne) — previously a survivor could react on a Deleted-User thread and the relay fanned out to all peers via undefined target-origins. Client Message.tsx withdraws add/toggle reaction affordances for dead DMs (existing reactions still display read-only). Finding 2: dmMembership purge test now runs with foreign_keys=ON (matches prod) and asserts dm_members/dm_messages cascade cleanup on channel purge. Finding 3: tombstone group-DM ownership transfer filters isDeleted=0 so ownership can never move to a tombstoned member; covered by a new test.
This commit is contained in:
@@ -4,7 +4,7 @@ import { getDb, schema } from '../db/index.js';
|
||||
import { generateSnowflake } from '../utils/snowflake.js';
|
||||
import { connectionManager } from './handler.js';
|
||||
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
|
||||
import { isMember, getChannelSpaceId, isDmMember, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
|
||||
import { isMember, getChannelSpaceId, isDmMember, isDeadOneOnOne, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
|
||||
import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.js';
|
||||
import { MAX_MESSAGE_LENGTH, type MessageWithUser, type Attachment, type DmMessageWithUser, type Embed, type Activity, type ActivityType, type ActivityTimestamps, type ActivityAssets, type ServerEvent, type DmCallUndeliverableFailure, type DmCallUndeliverableReason } from '@backspace/shared';
|
||||
import type { CallRelayResult, CallFanoutFailure } from '../utils/federationOutbox.js';
|
||||
@@ -1138,6 +1138,9 @@ function handleReactionAdd(event: Record<string, unknown>, userId: string, isFed
|
||||
if (isFederated) return;
|
||||
const dmMsg = db.select().from(schema.dmMessages).where(eq(schema.dmMessages.id, messageId)).get();
|
||||
if (!dmMsg || !isDmMember(dmMsg.dmChannelId, userId)) return;
|
||||
// Read-only enforcement: a dead 1-on-1 thread (partner tombstoned) accepts no
|
||||
// reaction mutations — the relay would fan out to all peers via undefined origins.
|
||||
if (isDeadOneOnOne(dmMsg.dmChannelId, userId)) return;
|
||||
|
||||
const reactionId = generateSnowflake();
|
||||
const now = Date.now();
|
||||
@@ -1224,6 +1227,9 @@ function handleReactionRemove(event: Record<string, unknown>, userId: string, is
|
||||
if (isFederated) return;
|
||||
const dmMsg = db.select().from(schema.dmMessages).where(eq(schema.dmMessages.id, messageId)).get();
|
||||
if (!dmMsg || !isDmMember(dmMsg.dmChannelId, userId)) return;
|
||||
// Read-only enforcement: a dead 1-on-1 thread (partner tombstoned) accepts no
|
||||
// reaction mutations — the relay would fan out to all peers via undefined origins.
|
||||
if (isDeadOneOnOne(dmMsg.dmChannelId, userId)) return;
|
||||
|
||||
const result = db.delete(schema.dmReactions)
|
||||
.where(and(
|
||||
|
||||
Reference in New Issue
Block a user