From 8de27fd39c1fd060e9a96131527469c54a3de823 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:20:08 +0200 Subject: [PATCH] feat(federation): add generateFederatedCallToken and federated room naming (FED-009) --- packages/server/src/routes/livekit.ts | 45 ++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/server/src/routes/livekit.ts b/packages/server/src/routes/livekit.ts index f2a15f95..c2c6ad64 100644 --- a/packages/server/src/routes/livekit.ts +++ b/packages/server/src/routes/livekit.ts @@ -4,6 +4,40 @@ import { authenticate } from '../utils/auth.js'; import { config } from '../config.js'; import { getChannelSpaceId, hasPermission, computePermissions, isDmMember, PermissionBits } from '../utils/permissions.js'; import type { LiveKitTokenRequest, LiveKitTokenResponse } from '@backspace/shared'; +import { getDb, schema } from '../db/index.js'; +import { eq } from 'drizzle-orm'; + +/** + * Generate a LiveKit token for a federated call participant. + * Uses homeUserId as identity (stable across instances). + * Short TTL (5 min) — must join quickly. + */ +export async function generateFederatedCallToken( + roomName: string, + homeUserId: string, + displayName: string, +): Promise { + const token = new AccessToken(config.livekit.apiKey!, config.livekit.apiSecret!, { + identity: `${homeUserId}:${displayName}`, + ttl: '5m', + }); + + token.addGrant({ + room: roomName, + roomJoin: true, + canPublish: true, + canPublishSources: [ + TrackSource.MICROPHONE, + TrackSource.CAMERA, + TrackSource.SCREEN_SHARE, + TrackSource.SCREEN_SHARE_AUDIO, + ], + canSubscribe: true, + canPublishData: true, + }); + + return token.toJwt(); +} export async function livekitRoutes(app: FastifyInstance): Promise { app.post<{ Body: LiveKitTokenRequest & { dmChannelId?: string } }>('/api/livekit/token', { @@ -27,7 +61,16 @@ export async function livekitRoutes(app: FastifyInstance): Promise { if (!isDmMember(dmChannelId, request.userId)) { return reply.code(403).send({ error: 'You are not a member of this DM channel', statusCode: 403 }); } - roomName = `dm-${dmChannelId}`; + + // Federated DMs use federatedId as room name (cross-instance stable). + // Local-only DMs use dm-{dmChannelId}. + const db = getDb(); + const channel = db.select({ federatedId: schema.dmChannels.federatedId }) + .from(schema.dmChannels) + .where(eq(schema.dmChannels.id, dmChannelId)) + .get(); + + roomName = channel?.federatedId ? channel.federatedId : `dm-${dmChannelId}`; } else if (channelId && typeof channelId === 'string') { // Space voice channel token const spaceId = getChannelSpaceId(channelId);