feat(federation): add generateFederatedCallToken and federated room naming (FED-009)

This commit is contained in:
Jannis Braun
2026-03-31 23:20:08 +02:00
parent 06d2c972b9
commit 8de27fd39c
+44 -1
View File
@@ -4,6 +4,40 @@ import { authenticate } from '../utils/auth.js';
import { config } from '../config.js'; import { config } from '../config.js';
import { getChannelSpaceId, hasPermission, computePermissions, isDmMember, PermissionBits } from '../utils/permissions.js'; import { getChannelSpaceId, hasPermission, computePermissions, isDmMember, PermissionBits } from '../utils/permissions.js';
import type { LiveKitTokenRequest, LiveKitTokenResponse } from '@backspace/shared'; 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<string> {
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<void> { export async function livekitRoutes(app: FastifyInstance): Promise<void> {
app.post<{ Body: LiveKitTokenRequest & { dmChannelId?: string } }>('/api/livekit/token', { app.post<{ Body: LiveKitTokenRequest & { dmChannelId?: string } }>('/api/livekit/token', {
@@ -27,7 +61,16 @@ export async function livekitRoutes(app: FastifyInstance): Promise<void> {
if (!isDmMember(dmChannelId, request.userId)) { if (!isDmMember(dmChannelId, request.userId)) {
return reply.code(403).send({ error: 'You are not a member of this DM channel', statusCode: 403 }); 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') { } else if (channelId && typeof channelId === 'string') {
// Space voice channel token // Space voice channel token
const spaceId = getChannelSpaceId(channelId); const spaceId = getChannelSpaceId(channelId);