feat: enhance room member token handling with participant metadata and moderation actions

Enhances token refresh with participant metadata support

Improves room member token generation to allow rebuilding token
metadata from current participant state in LiveKit, enabling
accurate permission and role handling after in-meeting upgrades
or downgrades. Adds support for in-meeting moderation actions
(promote/demote moderator) and updates token and context logic
to reflect dynamic role and permission changes for participants.
This commit is contained in:
juancarmore 2026-03-06 11:39:28 +01:00
parent eca3acbcf0
commit 1223e3d53b
7 changed files with 338 additions and 211 deletions

View File

@ -34,6 +34,4 @@ export interface MeetRoomMemberTokenOptions {
participantName?: string;
/** Identity of the participant */
participantIdentity?: string;
/** Indicates if the room has captions enabled */
roomWithCaptions?: boolean;
}

View File

@ -7,6 +7,7 @@ import {
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions,
MeetRoomMemberUIBadge,
SortOrder
} from '@openvidu-meet/typings';
import { z } from 'zod';
@ -141,7 +142,8 @@ export const RoomMemberTokenOptionsSchema: z.ZodType<MeetRoomMemberTokenOptions>
secret: z.string().optional(),
joinMeeting: z.boolean().optional().default(false),
participantName: z.string().optional(),
participantIdentity: z.string().optional()
participantIdentity: z.string().optional(),
useParticipantMetadata: z.boolean().optional().default(false)
})
.refine(
(data) => {
@ -156,10 +158,11 @@ export const RoomMemberTokenOptionsSchema: z.ZodType<MeetRoomMemberTokenOptions>
export const RoomMemberTokenMetadataSchema: z.ZodType<MeetRoomMemberTokenMetadata> = z.object({
iat: z.number(),
livekitUrl: z.string().url('LiveKit URL must be a valid URL'),
roomId: z.string(),
memberId: z.string().optional(),
baseRole: RoomMemberRoleSchema,
customPermissions: PartialMeetPermissionsSchema.optional(),
effectivePermissions: MeetPermissionsSchema
userId: z.string().optional(),
permissions: MeetPermissionsSchema,
badge: z.nativeEnum(MeetRoomMemberUIBadge),
isPromotedModerator: z.boolean().optional(),
livekitUrl: z.string().url('LiveKit URL must be a valid URL').optional()
});

View File

@ -1,5 +1,6 @@
import {
LiveKitPermissions,
MeetParticipantModerationAction,
MeetRoomMember,
MeetRoomMemberField,
MeetRoomMemberFilters,
@ -8,6 +9,7 @@ import {
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions,
MeetRoomMemberUIBadge,
MeetRoomRoles,
MeetRoomStatus,
MeetUserRole,
@ -43,6 +45,17 @@ import { RoomService } from './room.service.js';
import { TokenService } from './token.service.js';
import { UserService } from './user.service.js';
interface ResolvedPermissionSource {
memberId?: string;
userId?: string;
permissions: MeetRoomMemberPermissions;
badge?: MeetRoomMemberUIBadge;
}
interface ParticipantMeetingMetadata extends MeetRoomMemberTokenMetadata {
originalPermissions?: MeetRoomMemberPermissions;
}
/**
* Service for managing room members and meeting participants.
*/
@ -391,125 +404,59 @@ export class RoomMemberService {
* @returns A promise that resolves to the generated token
*/
async generateOrRefreshRoomMemberToken(roomId: string, tokenOptions: MeetRoomMemberTokenOptions): Promise<string> {
const { secret, joinMeeting = false, participantName, participantIdentity } = tokenOptions;
const {
secret,
joinMeeting = false,
participantName,
participantIdentity,
useParticipantMetadata = false
} = tokenOptions;
let baseRole: MeetRoomMemberRole;
let customPermissions: Partial<MeetRoomMemberPermissions> | undefined = undefined;
let effectivePermissions: MeetRoomMemberPermissions;
let memberId: string | undefined;
let userId: string | undefined;
if (secret) {
// Case 1: Secret provided (Anonymous access or External Member)
const isExternalMemberId = secret.startsWith('ext-');
if (isExternalMemberId) {
// If secret is a external member ID, fetch the member and assign their role and permissions
const member = await this.getRoomMember(roomId, secret);
if (!member) {
throw errorRoomMemberNotFound(roomId, secret);
}
memberId = member.memberId;
baseRole = member.baseRole;
customPermissions = member.customPermissions;
effectivePermissions = member.effectivePermissions;
} else {
// If secret matches anonymous access URL secret, assign role and permissions based on it
const anonymousAccess = await this.resolveAnonymousAccessBySecret(roomId, secret);
baseRole = anonymousAccess.baseRole;
effectivePermissions = anonymousAccess.effectivePermissions;
}
} else {
// Case 2: Authenticated user
const user = this.requestSessionService.getAuthenticatedUser();
if (!user) {
if (joinMeeting && participantName && useParticipantMetadata) {
if (!participantIdentity) {
// TODO: Consider throwing a more specific error indicating that participant identity is required for token refresh when using participant metadata
throw errorUnauthorized();
}
userId = user.userId;
// Check if user is admin or owner
const isOwner = await this.roomService.isRoomOwner(roomId, user.userId);
if (user.role === MeetUserRole.ADMIN || isOwner) {
// Admins and owners have MODERATOR role with full permissions
baseRole = MeetRoomMemberRole.MODERATOR;
effectivePermissions = this.getAllPermissions();
} else {
// If user is a member, fetch their role and permissions
const member = await this.getRoomMember(roomId, user.userId);
if (!member) {
throw errorInsufficientPermissions();
}
memberId = user.userId;
baseRole = member.baseRole;
customPermissions = member.customPermissions;
effectivePermissions = member.effectivePermissions;
}
const tokenMetadata = await this.resolveTokenMetadataFromParticipant(roomId, participantIdentity);
return this.generateTokenForJoiningMeeting(roomId, tokenMetadata, participantName, participantIdentity);
}
const [secretSource, authenticatedSource, room] = await Promise.all([
secret ? this.resolvePermissionSourceFromSecret(roomId, secret) : Promise.resolve(undefined),
this.resolvePermissionSourceFromAuthenticatedUser(roomId),
this.roomService.getMeetRoom(roomId, ['roles'])
]);
if (!secretSource && !authenticatedSource) {
throw errorUnauthorized();
}
const mergedPermissions = this.mergePermissions(secretSource?.permissions, authenticatedSource?.permissions);
let badge = authenticatedSource?.badge;
if (!badge) {
badge = this.resolveBadgeFromPermissions(mergedPermissions, room.roles.moderator.permissions);
}
const tokenMetadata: MeetRoomMemberTokenMetadata = {
iat: Date.now(),
roomId,
memberId: secretSource?.memberId || authenticatedSource?.memberId,
userId: authenticatedSource?.userId,
permissions: mergedPermissions,
badge
};
if (joinMeeting && participantName) {
return this.generateTokenForJoiningMeeting(
roomId,
baseRole,
effectivePermissions,
participantName,
participantIdentity,
customPermissions,
memberId,
userId
);
tokenMetadata.livekitUrl = MEET_ENV.LIVEKIT_URL;
return this.generateTokenForJoiningMeeting(roomId, tokenMetadata, participantName, participantIdentity);
}
return this.generateToken(roomId, baseRole, effectivePermissions, customPermissions, memberId);
}
/**
* Resolves anonymous access and effective permissions from a room secret.
*
* - Moderator and speaker secrets map to their room role permissions.
* - Recording secret maps to read-only recording permissions.
*/
protected async resolveAnonymousAccessBySecret(
roomId: string,
secret: string
): Promise<{ baseRole: MeetRoomMemberRole; effectivePermissions: MeetRoomMemberPermissions }> {
const { roles, access } = await this.roomService.getMeetRoom(roomId, ['roles', 'access']);
const { moderatorSecret, speakerSecret, recordingSecret } = MeetRoomHelper.extractSecretsFromRoom(access);
const anonymousRole: MeetRoomMemberRole | 'recording' | undefined =
secret === moderatorSecret
? MeetRoomMemberRole.MODERATOR
: secret === speakerSecret
? MeetRoomMemberRole.SPEAKER
: secret === recordingSecret
? 'recording'
: undefined;
if (!anonymousRole) {
throw errorInvalidRoomSecret(roomId, secret);
}
if (!access.anonymous[anonymousRole].enabled) {
throw errorAnonymousAccessDisabled(roomId, anonymousRole);
}
if (anonymousRole === 'recording') {
return {
baseRole: MeetRoomMemberRole.SPEAKER,
effectivePermissions: this.getRecordingReadOnlyPermissions()
};
}
return {
baseRole: anonymousRole,
effectivePermissions: roles[anonymousRole].permissions
};
this.logger.verbose(
`Generating room member token for accessing room resources but not joining a meeting for room '${roomId}'`
);
return this.tokenService.generateRoomMemberToken({ tokenMetadata });
}
/**
@ -518,23 +465,19 @@ export class RoomMemberService {
*/
protected async generateTokenForJoiningMeeting(
roomId: string,
baseRole: MeetRoomMemberRole,
effectivePermissions: MeetRoomMemberPermissions,
tokenMetadata: MeetRoomMemberTokenMetadata,
participantName: string,
participantIdentity?: string,
customPermissions?: Partial<MeetRoomMemberPermissions>,
memberId?: string,
userId?: string
participantIdentity?: string
): Promise<string> {
// Check that room is open
const { status, config } = await this.roomService.getMeetRoom(roomId, ['status', 'config']);
const { status } = await this.roomService.getMeetRoom(roomId, ['status']);
if (status === MeetRoomStatus.CLOSED) {
throw errorRoomClosed(roomId);
}
// Check that member has permission to join meeting
if (!effectivePermissions.canJoinMeeting) {
if (!tokenMetadata.permissions.canJoinMeeting) {
throw errorInsufficientPermissions();
}
@ -558,11 +501,11 @@ export class RoomMemberService {
// Create the Livekit room if it doesn't exist
await this.roomService.createLivekitRoom(roomId);
if (memberId || userId) {
if (tokenMetadata.memberId || tokenMetadata.userId) {
// Use memberId as participant identity for identified members
// (registered users or external members with a record in the database)
// Use userId as participant identity for registered users without a member record
participantIdentity = memberId || userId;
participantIdentity = tokenMetadata.memberId || tokenMetadata.userId;
} else {
// For anonymous users, create a unique participant identity based on the provided participant name
const identityPrefix = this.createParticipantIdentityPrefixFromName(participantName) || 'participant';
@ -583,56 +526,166 @@ export class RoomMemberService {
}
}
const livekitPermissions = this.getLiveKitPermissions(roomId, effectivePermissions);
const tokenMetadata: MeetRoomMemberTokenMetadata = {
iat: Date.now(),
livekitUrl: MEET_ENV.LIVEKIT_URL,
roomId,
memberId,
baseRole,
customPermissions,
effectivePermissions
};
const roomWithCaptions = config.captions.enabled;
// Generate token with participant name
const livekitPermissions = this.getLiveKitPermissions(roomId, tokenMetadata.permissions);
return this.tokenService.generateRoomMemberToken({
tokenMetadata,
livekitPermissions,
participantName,
participantIdentity,
roomWithCaptions
participantIdentity
});
}
/**
* Generates a token for accessing room resources but not joining a meeting.
*/
protected async generateToken(
protected async resolvePermissionSourceFromSecret(
roomId: string,
baseRole: MeetRoomMemberRole,
effectivePermissions: MeetRoomMemberPermissions,
customPermissions?: Partial<MeetRoomMemberPermissions>,
memberId?: string
): Promise<string> {
this.logger.verbose(
`Generating room member token for accessing room resources but not joining a meeting for room '${roomId}'`
secret: string
): Promise<ResolvedPermissionSource> {
const isExternalMemberId = secret.startsWith('ext-');
if (isExternalMemberId) {
const member = await this.getRoomMember(roomId, secret);
if (!member) {
throw errorRoomMemberNotFound(roomId, secret);
}
return {
memberId: member.memberId,
permissions: member.effectivePermissions
};
}
return this.resolveAnonymousAccessBySecret(roomId, secret);
}
/**
* Resolves anonymous access and effective permissions from a room secret.
*
* - Moderator and speaker secrets map to their room role permissions.
* - Recording secret maps to read-only recording permissions.
*/
protected async resolveAnonymousAccessBySecret(roomId: string, secret: string): Promise<ResolvedPermissionSource> {
const { roles, access } = await this.roomService.getMeetRoom(roomId, ['roles', 'access']);
const { moderatorSecret, speakerSecret, recordingSecret } = MeetRoomHelper.extractSecretsFromRoom(access);
const anonymousRole: MeetRoomMemberRole | 'recording' | undefined =
secret === moderatorSecret
? MeetRoomMemberRole.MODERATOR
: secret === speakerSecret
? MeetRoomMemberRole.SPEAKER
: secret === recordingSecret
? 'recording'
: undefined;
if (!anonymousRole) {
throw errorInvalidRoomSecret(roomId, secret);
}
if (!access.anonymous[anonymousRole].enabled) {
throw errorAnonymousAccessDisabled(roomId, anonymousRole);
}
if (anonymousRole === 'recording') {
return {
permissions: this.getRecordingReadOnlyPermissions()
};
}
return {
permissions: roles[anonymousRole].permissions
};
}
protected async resolvePermissionSourceFromAuthenticatedUser(
roomId: string
): Promise<ResolvedPermissionSource | undefined> {
const user = this.requestSessionService.getAuthenticatedUser();
if (!user) {
return undefined;
}
const isAdmin = user.role === MeetUserRole.ADMIN;
const isOwner = await this.roomService.isRoomOwner(roomId, user.userId);
if (isAdmin || isOwner) {
// Admins and room owner get all permissions without needing a member record in the database
return {
userId: user.userId,
permissions: this.getAllPermissions(),
badge: isAdmin ? MeetRoomMemberUIBadge.ADMIN : MeetRoomMemberUIBadge.OWNER
};
}
const member = await this.getRoomMember(roomId, user.userId);
if (!member) {
return undefined;
}
return {
memberId: user.userId,
userId: user.userId,
permissions: member.effectivePermissions
};
}
protected mergePermissions(
first?: MeetRoomMemberPermissions,
second?: MeetRoomMemberPermissions
): MeetRoomMemberPermissions {
const merged = this.getNoPermissions();
const sources = [first, second].filter(
(permissions): permissions is MeetRoomMemberPermissions => !!permissions
);
const tokenMetadata: MeetRoomMemberTokenMetadata = {
iat: Date.now(),
livekitUrl: MEET_ENV.LIVEKIT_URL,
roomId,
memberId,
baseRole,
customPermissions,
effectivePermissions
};
for (const source of sources) {
for (const [key, value] of Object.entries(source) as [keyof MeetRoomMemberPermissions, boolean][]) {
merged[key] = merged[key] || value;
}
}
// Generate token without LiveKit permissions and participant name
return this.tokenService.generateRoomMemberToken({
tokenMetadata
return merged;
}
protected resolveBadgeFromPermissions(
permissions: MeetRoomMemberPermissions,
moderatorPermissions: MeetRoomMemberPermissions
): MeetRoomMemberUIBadge {
const hasModeratorPermissions = Object.entries(moderatorPermissions).every(([permission, allowed]) => {
if (!allowed) {
return true;
}
return permissions[permission as keyof MeetRoomMemberPermissions] === true;
});
return hasModeratorPermissions ? MeetRoomMemberUIBadge.MODERATOR : MeetRoomMemberUIBadge.OTHER;
}
protected async resolveTokenMetadataFromParticipant(
roomId: string,
participantIdentity: string
): Promise<MeetRoomMemberTokenMetadata> {
const sessionParticipantIdentity = this.requestSessionService.getParticipantIdentity();
if (!sessionParticipantIdentity || sessionParticipantIdentity !== participantIdentity) {
throw errorInsufficientPermissions();
}
const participant = await this.getParticipantFromMeeting(roomId, participantIdentity);
const participantMetadata = this.parseParticipantMeetingMetadata(participant.metadata);
return {
iat: Date.now(),
roomId,
memberId: participantMetadata.memberId,
userId: participantMetadata.userId,
permissions: participantMetadata.permissions,
badge: participantMetadata.badge,
isPromotedModerator: participantMetadata.isPromotedModerator,
livekitUrl: participantMetadata.livekitUrl
};
}
/**
@ -723,6 +776,58 @@ export class RoomMemberService {
return livekitPermissions;
}
async updateParticipantRole(
roomId: string,
participantIdentity: string,
action: MeetParticipantModerationAction
): Promise<void> {
try {
const { roles } = await this.roomService.getMeetRoom(roomId, ['roles']);
const participant = await this.getParticipantFromMeeting(roomId, participantIdentity);
const metadata = this.parseParticipantMeetingMetadata(participant.metadata);
if (action === MeetParticipantModerationAction.UPGRADE) {
if (metadata.badge !== MeetRoomMemberUIBadge.OTHER) {
// TODO: Consider throwing a more specific error indicating that only participants with OTHER badge can be promoted to moderator
throw errorInsufficientPermissions();
}
metadata.originalPermissions = metadata.permissions;
metadata.permissions = this.mergePermissions(
metadata.permissions,
roles[MeetRoomMemberRole.MODERATOR].permissions
);
metadata.badge = MeetRoomMemberUIBadge.MODERATOR;
metadata.isPromotedModerator = true;
} else {
if (
metadata.badge !== MeetRoomMemberUIBadge.MODERATOR ||
!metadata.isPromotedModerator ||
!metadata.originalPermissions
) {
// TODO: Consider throwing a more specific error indicating that only participants with MODERATOR badge
// that were promoted (not original moderators) can be demoted back to their original permissions
throw errorInsufficientPermissions();
}
metadata.permissions = metadata.originalPermissions;
metadata.badge = MeetRoomMemberUIBadge.OTHER;
metadata.isPromotedModerator = undefined;
delete metadata.originalPermissions;
}
await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata));
await this.frontendEventService.sendParticipantRoleUpdatedSignal(
roomId,
participantIdentity,
metadata.badge
);
} catch (error) {
this.logger.error('Error applying participant moderation action:', error);
throw error;
}
}
/**
* Kicks multiple members from a meeting in batches.
* This method processes the kicks in parallel batches to avoid overwhelming the system.
@ -795,39 +900,6 @@ export class RoomMemberService {
return this.livekitService.deleteParticipant(roomId, participantIdentity);
}
async updateParticipantRole(
roomId: string,
participantIdentity: string,
newRole: MeetRoomMemberRole
): Promise<void> {
try {
const { roles, access } = await this.roomService.getMeetRoom(roomId, ['roles', 'access']);
const participant = await this.getParticipantFromMeeting(roomId, participantIdentity);
const metadata: MeetRoomMemberTokenMetadata = this.tokenService.parseRoomMemberTokenMetadata(
participant.metadata
);
// Update role and permissions in metadata
metadata.baseRole = newRole;
metadata.customPermissions = undefined;
metadata.effectivePermissions = roles[newRole].permissions;
await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata));
const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(access);
const secret = newRole === MeetRoomMemberRole.MODERATOR ? moderatorSecret : speakerSecret;
await this.frontendEventService.sendParticipantRoleUpdatedSignal(
roomId,
participantIdentity,
newRole,
secret
);
} catch (error) {
this.logger.error('Error updating participant role:', error);
throw error;
}
}
protected async existsParticipantInMeeting(roomId: string, participantIdentity: string): Promise<boolean> {
this.logger.verbose(`Checking if participant '${participantIdentity}' exists in room '${roomId}'`);
return this.livekitService.participantExists(roomId, participantIdentity);
@ -838,6 +910,16 @@ export class RoomMemberService {
return this.livekitService.getParticipant(roomId, participantIdentity);
}
protected parseParticipantMeetingMetadata(metadata: string): ParticipantMeetingMetadata {
const parsed = JSON.parse(metadata || '{}') as ParticipantMeetingMetadata;
const normalized = this.tokenService.parseRoomMemberTokenMetadata(JSON.stringify(parsed));
return {
...parsed,
...normalized
};
}
/**
* Creates a sanitized participant identity prefix from the given participant name.
*

View File

@ -75,13 +75,16 @@ export class RoomMemberInterceptorErrorHandlerService implements HttpErrorHandle
const participantName = this.roomMemberContextService.participantName();
const participantIdentity = this.roomMemberContextService.participantIdentity();
const joinMeeting = !!participantIdentity; // Grant join permission if identity is set
// If the member is a promoted moderator, we need to use LiveKit participant metadata to get the updated permissions in the token
const useParticipantMetadata = this.roomMemberContextService.isPromotedModerator();
return from(
this.roomMemberContextService.generateToken(roomId, {
secret,
joinMeeting,
participantName,
participantIdentity
participantIdentity,
useParticipantMetadata
})
).pipe(
switchMap(() => {

View File

@ -3,7 +3,8 @@ import {
MeetRoomMember,
MeetRoomMemberPermissions,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions
MeetRoomMemberTokenOptions,
MeetRoomMemberUIBadge
} from '@openvidu-meet/typings';
import { E2eeService, LoggerService } from 'openvidu-components-angular';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
@ -23,6 +24,8 @@ export class RoomMemberContextService {
private readonly _participantName = signal<string | undefined>(undefined);
private readonly _isParticipantNameFromUrl = signal<boolean>(false);
private readonly _participantIdentity = signal<string | undefined>(undefined);
private readonly _memberBadge = signal<MeetRoomMemberUIBadge>(MeetRoomMemberUIBadge.OTHER);
private readonly _isPromotedModerator = signal<boolean>(false);
private readonly _permissions = signal<MeetRoomMemberPermissions | undefined>(undefined);
private readonly _member = signal<MeetRoomMember | undefined>(undefined);
@ -40,6 +43,10 @@ export class RoomMemberContextService {
readonly member = this._member.asReadonly();
/** Computed signal for the room member's display name */
readonly memberName = computed(() => this._member()?.name);
/** Readonly signal for the room member's UI badge */
readonly memberBadge = this._memberBadge.asReadonly();
/** Readonly signal for whether the room member is a promoted moderator */
readonly isPromotedModerator = this._isPromotedModerator.asReadonly();
protected log;
@ -83,6 +90,14 @@ export class RoomMemberContextService {
}
}
/**
* Sets the promoted moderator status for the current room member.
* @param isPromoted - A boolean indicating whether the member has been promoted to moderator status.
*/
setPromotedModerator(isPromoted: boolean): void {
this._isPromotedModerator.set(isPromoted);
}
/**
* Checks if the current room member has a specific permission.
*
@ -132,7 +147,8 @@ export class RoomMemberContextService {
this._participantIdentity.set(decodedToken.sub);
}
this._permissions.set(metadata.effectivePermissions);
this._permissions.set(metadata.permissions);
this._memberBadge.set(metadata.badge);
// If token contains memberId, fetch and store member info
if (metadata.memberId) {
@ -143,7 +159,6 @@ export class RoomMemberContextService {
this.log.w('Could not fetch member info:', error);
}
}
} catch (error) {
this.log.e('Error decoding room member token:', error);
throw new Error('Invalid room member token');
@ -159,6 +174,8 @@ export class RoomMemberContextService {
this._isParticipantNameFromUrl.set(false);
this._participantIdentity.set(undefined);
this._permissions.set(undefined);
this._memberBadge.set(MeetRoomMemberUIBadge.OTHER);
this._isPromotedModerator.set(false);
this._member.set(undefined);
}
}

View File

@ -39,4 +39,19 @@ export interface MeetRoomMemberTokenOptions {
* Required when refreshing an existing token used to join a meeting.
*/
participantIdentity?: string;
/**
* If true, the token metadata is rebuilt from the current participant metadata in LiveKit.
* Used when refreshing tokens after in-meeting permission updates.
*/
useParticipantMetadata?: boolean;
}
/**
* Enum representing moderation actions that can be performed on a meeting participant.
*/
export enum MeetParticipantModerationAction {
/** Action to promote a participant to moderator role */
UPGRADE = 'upgrade',
/** Action to demote a participant from moderator role and revert to original role */
DOWNGRADE = 'downgrade'
}

View File

@ -1,5 +1,5 @@
import { MeetRoomMemberPermissions } from '../database/room-member-permissions.js';
import { MeetRoomMember, MeetRoomMemberRole } from '../database/room-member.entity.js';
import { MeetRoomMember } from '../database/room-member.entity.js';
import { SortAndPagination, SortableFieldKey } from './sort-pagination.js';
/**
@ -53,23 +53,32 @@ export interface MeetRoomMemberFilters extends SortAndPagination<MeetRoomMemberS
export interface MeetRoomMemberTokenMetadata {
/** Token issued at timestamp (milliseconds since epoch) */
iat: number;
/** URL of the LiveKit server to connect to */
livekitUrl: string;
/** Unique identifier for the room */
roomId: string;
/** Unique identifier for the member if defined */
memberId?: string;
/** Base role assigned to the member. See {@link MeetRoomMemberRole} for details. */
baseRole: MeetRoomMemberRole;
/** Custom permissions for the member (overrides base role permissions). See {@link MeetRoomMemberPermissions} for details. */
customPermissions?: Partial<MeetRoomMemberPermissions>;
/** Effective permissions for the member (combination of base role and custom permissions). See {@link MeetRoomMemberPermissions} for details. */
effectivePermissions: MeetRoomMemberPermissions;
/** Unique identifier for the user if defined */
userId?: string;
/** Effective permissions for the member. */
permissions: MeetRoomMemberPermissions;
/** Visual badge/category used in participant UI. */
badge: MeetRoomMemberUIBadge;
/** Indicates if participant has been promoted to moderator during the meeting and is not originally a moderator. */
isPromotedModerator?: boolean;
/** URL of the LiveKit server to connect to when joining the meeting */
livekitUrl?: string;
}
/**
* UI badge/category for room members, used to visually distinguish roles in the participant list and other UI elements.
*/
export enum MeetRoomMemberUIBadge {
/** Owner badge, typically for the creator of the room */
OWNER = 'owner',
/** Admin badge, typically for users with administrative privileges */
ADMIN = 'admin',
/** Moderator badge, typically for users with moderation privileges */
MODERATOR = 'moderator',
/** Other badge, typically for regular participants without special privileges */
OTHER = 'other'
}