diff --git a/packages/server/src/routes/dm.ts b/packages/server/src/routes/dm.ts index aaccc9f5..eed954e2 100644 --- a/packages/server/src/routes/dm.ts +++ b/packages/server/src/routes/dm.ts @@ -1,7 +1,7 @@ import type { FastifyInstance } from 'fastify'; import { eq, and, or, desc, lt, inArray, isNull, sql } from 'drizzle-orm'; import { getDb, schema } from '../db/index.js'; -import { authenticate, requireLocalUser } from '../utils/auth.js'; +import { authenticate } from '../utils/auth.js'; import { generateSnowflake } from '../utils/snowflake.js'; import { isDmMember } from '../utils/permissions.js'; import { connectionManager } from '../ws/handler.js'; @@ -243,9 +243,8 @@ export function broadcastDmMessage(dmChannelId: string, message: DmMessageWithUs } export async function dmRoutes(app: FastifyInstance): Promise { - // Centralized auth + federation gating for all DM routes + // Centralized auth for all DM routes app.addHook('preHandler', authenticate); - app.addHook('preHandler', requireLocalUser); // GET /api/dm - List user's DM channels app.get('/api/dm', async (request, reply) => { diff --git a/packages/server/src/ws/events.ts b/packages/server/src/ws/events.ts index 6691227b..47fea4e8 100644 --- a/packages/server/src/ws/events.ts +++ b/packages/server/src/ws/events.ts @@ -145,11 +145,12 @@ export function handleClientEvent( ): void { const type = event.type as string; - // Federation gating: federated users must use their home instance for DM operations - if (isFederated && type.startsWith('dm_')) { + // Federation gating: DM calls remain blocked for federated users (separate scope) + const DM_CALL_EVENTS = ['dm_call_start', 'dm_call_accept', 'dm_call_reject', 'dm_call_end']; + if (isFederated && DM_CALL_EVENTS.includes(type)) { connectionManager.sendToUser(userId, { type: 'error', - message: 'Federated users must use their home instance for DM operations', + message: 'Federated users cannot use DM calls on remote instances', }); return; } @@ -1278,7 +1279,6 @@ function handleChannelAck(event: Record, userId: string, isFede if (spaceId) { if (!isMember(spaceId, userId)) return; } else { - if (isFederated) return; if (!isDmMember(channelId, userId)) return; } @@ -1333,7 +1333,6 @@ function handleMarkUnread(event: Record, userId: string, isFede if (spaceId) { if (!isMember(spaceId, userId)) return; } else { - if (isFederated) return; if (!isDmMember(channelId, userId)) return; } diff --git a/packages/server/src/ws/handler.ts b/packages/server/src/ws/handler.ts index 247b6902..43412e5e 100644 --- a/packages/server/src/ws/handler.ts +++ b/packages/server/src/ws/handler.ts @@ -1108,8 +1108,8 @@ function buildReadyPayload(userId: string): { // Store user's space IDs for broadcasting connectionManager.setUserSpaces(userId, spaceIds); - // Get DM channels — skip entirely for federated users (they get DMs from their home instance) - const dmMemberships = isFederated ? [] : db.select() + // Get DM channels + const dmMemberships = db.select() .from(schema.dmMembers) .where(and( eq(schema.dmMembers.userId, userId), @@ -1198,6 +1198,7 @@ function buildReadyPayload(userId: string): { dmChannels.push({ id: dmChannel.id, + federatedId: dmChannel.federatedId ?? null, ownerId: dmChannel.ownerId ?? null, createdAt: dmChannel.createdAt, members, @@ -1214,6 +1215,11 @@ function buildReadyPayload(userId: string): { } + // Include DM channel IDs in the visible set for read state filtering + for (const dm of dmChannels) { + visibleChannelIdSet.add(dm.id); + } + // Get Space Folders const folderRows = db.select() .from(schema.spaceFolders)