From 1fda5c79202ea51c2c007ec03aa2fafbcc28af67 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 24 Nov 2025 10:17:29 +0100 Subject: [PATCH 01/76] remove otel to see what impact it has on tests. --- src/initializer.tsx | 11 - src/otel/OTelCall.ts | 188 ------- src/otel/OTelCallAbstractMediaStreamSpan.ts | 69 --- src/otel/OTelCallFeedMediaStreamSpan.ts | 64 --- src/otel/OTelCallMediaStreamTrackSpan.ts | 69 --- .../OTelCallTransceiverMediaStreamSpan.ts | 61 --- src/otel/OTelGroupCallMembership.ts | 477 ------------------ src/otel/ObjectFlattener.test.ts | 266 ---------- src/otel/ObjectFlattener.ts | 100 ---- src/otel/otel.test.ts | 79 --- src/otel/otel.ts | 117 ----- src/room/InCallView.tsx | 2 - src/settings/submit-rageshake.ts | 9 - 13 files changed, 1512 deletions(-) delete mode 100644 src/otel/OTelCall.ts delete mode 100644 src/otel/OTelCallAbstractMediaStreamSpan.ts delete mode 100644 src/otel/OTelCallFeedMediaStreamSpan.ts delete mode 100644 src/otel/OTelCallMediaStreamTrackSpan.ts delete mode 100644 src/otel/OTelCallTransceiverMediaStreamSpan.ts delete mode 100644 src/otel/OTelGroupCallMembership.ts delete mode 100644 src/otel/ObjectFlattener.test.ts delete mode 100644 src/otel/ObjectFlattener.ts delete mode 100644 src/otel/otel.test.ts delete mode 100644 src/otel/otel.ts diff --git a/src/initializer.tsx b/src/initializer.tsx index d0797e9d..2ecd1162 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -26,7 +26,6 @@ import { import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; -import { ElementCallOpenTelemetry } from "./otel/otel"; import { platform } from "./Platform"; import { isFailure } from "./utils/fetch"; @@ -101,7 +100,6 @@ enum LoadState { class DependencyLoadStates { public config: LoadState = LoadState.None; public sentry: LoadState = LoadState.None; - public openTelemetry: LoadState = LoadState.None; public allDepsAreLoaded(): boolean { return !Object.values(this).some((s) => s !== LoadState.Loaded); @@ -266,15 +264,6 @@ export class Initializer { this.loadStates.sentry = LoadState.Loaded; } - // OpenTelemetry (also only after config loaded) - if ( - this.loadStates.openTelemetry === LoadState.None && - this.loadStates.config === LoadState.Loaded - ) { - ElementCallOpenTelemetry.globalInit(); - this.loadStates.openTelemetry = LoadState.Loaded; - } - if (this.loadStates.allDepsAreLoaded()) { // resolve if there is no dependency that is not loaded resolve(); diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts deleted file mode 100644 index e70cedf2..00000000 --- a/src/otel/OTelCall.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type Span } from "@opentelemetry/api"; -import { type MatrixCall } from "matrix-js-sdk"; -import { CallEvent } from "matrix-js-sdk/lib/webrtc/call"; -import { - type TransceiverStats, - type CallFeedStats, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { ObjectFlattener } from "./ObjectFlattener"; -import { ElementCallOpenTelemetry } from "./otel"; -import { type OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; -import { OTelCallTransceiverMediaStreamSpan } from "./OTelCallTransceiverMediaStreamSpan"; -import { OTelCallFeedMediaStreamSpan } from "./OTelCallFeedMediaStreamSpan"; - -type StreamId = string; -type MID = string; - -/** - * Tracks an individual call within a group call, either to a full-mesh peer or a focus - */ -export class OTelCall { - private readonly trackFeedSpan = new Map< - StreamId, - OTelCallAbstractMediaStreamSpan - >(); - private readonly trackTransceiverSpan = new Map< - MID, - OTelCallAbstractMediaStreamSpan - >(); - - public constructor( - public userId: string, - public deviceId: string, - public call: MatrixCall, - public span: Span, - ) { - if (call.peerConn) { - this.addCallPeerConnListeners(); - } else { - this.call.once( - CallEvent.PeerConnectionCreated, - this.addCallPeerConnListeners, - ); - } - } - - public dispose(): void { - this.call.peerConn?.removeEventListener( - "connectionstatechange", - this.onCallConnectionStateChanged, - ); - this.call.peerConn?.removeEventListener( - "signalingstatechange", - this.onCallSignalingStateChanged, - ); - this.call.peerConn?.removeEventListener( - "iceconnectionstatechange", - this.onIceConnectionStateChanged, - ); - this.call.peerConn?.removeEventListener( - "icegatheringstatechange", - this.onIceGatheringStateChanged, - ); - this.call.peerConn?.removeEventListener( - "icecandidateerror", - this.onIceCandidateError, - ); - } - - private addCallPeerConnListeners = (): void => { - this.call.peerConn?.addEventListener( - "connectionstatechange", - this.onCallConnectionStateChanged, - ); - this.call.peerConn?.addEventListener( - "signalingstatechange", - this.onCallSignalingStateChanged, - ); - this.call.peerConn?.addEventListener( - "iceconnectionstatechange", - this.onIceConnectionStateChanged, - ); - this.call.peerConn?.addEventListener( - "icegatheringstatechange", - this.onIceGatheringStateChanged, - ); - this.call.peerConn?.addEventListener( - "icecandidateerror", - this.onIceCandidateError, - ); - }; - - public onCallConnectionStateChanged = (): void => { - this.span.addEvent("matrix.call.callConnectionStateChange", { - callConnectionState: this.call.peerConn?.connectionState, - }); - }; - - public onCallSignalingStateChanged = (): void => { - this.span.addEvent("matrix.call.callSignalingStateChange", { - callSignalingState: this.call.peerConn?.signalingState, - }); - }; - - public onIceConnectionStateChanged = (): void => { - this.span.addEvent("matrix.call.iceConnectionStateChange", { - iceConnectionState: this.call.peerConn?.iceConnectionState, - }); - }; - - public onIceGatheringStateChanged = (): void => { - this.span.addEvent("matrix.call.iceGatheringStateChange", { - iceGatheringState: this.call.peerConn?.iceGatheringState, - }); - }; - - public onIceCandidateError = (ev: Event): void => { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive(ev, flatObject, "error.", 0); - - this.span.addEvent("matrix.call.iceCandidateError", flatObject); - }; - - public onCallFeedStats(callFeeds: CallFeedStats[]): void { - let prvFeeds: StreamId[] = [...this.trackFeedSpan.keys()]; - - callFeeds.forEach((feed) => { - if (!this.trackFeedSpan.has(feed.stream)) { - this.trackFeedSpan.set( - feed.stream, - new OTelCallFeedMediaStreamSpan( - ElementCallOpenTelemetry.instance, - this.span, - feed, - ), - ); - } - this.trackFeedSpan.get(feed.stream)?.update(feed); - prvFeeds = prvFeeds.filter((prvStreamId) => prvStreamId !== feed.stream); - }); - - prvFeeds.forEach((prvStreamId) => { - this.trackFeedSpan.get(prvStreamId)?.end(); - this.trackFeedSpan.delete(prvStreamId); - }); - } - - public onTransceiverStats(transceiverStats: TransceiverStats[]): void { - let prvTransSpan: MID[] = [...this.trackTransceiverSpan.keys()]; - - transceiverStats.forEach((transStats) => { - if (!this.trackTransceiverSpan.has(transStats.mid)) { - this.trackTransceiverSpan.set( - transStats.mid, - new OTelCallTransceiverMediaStreamSpan( - ElementCallOpenTelemetry.instance, - this.span, - transStats, - ), - ); - } - this.trackTransceiverSpan.get(transStats.mid)?.update(transStats); - prvTransSpan = prvTransSpan.filter( - (prvStreamId) => prvStreamId !== transStats.mid, - ); - }); - - prvTransSpan.forEach((prvMID) => { - this.trackTransceiverSpan.get(prvMID)?.end(); - this.trackTransceiverSpan.delete(prvMID); - }); - } - - public end(): void { - this.trackFeedSpan.forEach((feedSpan) => feedSpan.end()); - this.trackTransceiverSpan.forEach((transceiverSpan) => - transceiverSpan.end(), - ); - this.span.end(); - } -} diff --git a/src/otel/OTelCallAbstractMediaStreamSpan.ts b/src/otel/OTelCallAbstractMediaStreamSpan.ts deleted file mode 100644 index 69e41547..00000000 --- a/src/otel/OTelCallAbstractMediaStreamSpan.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import opentelemetry, { type Span } from "@opentelemetry/api"; -import { type TrackStats } from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { type ElementCallOpenTelemetry } from "./otel"; -import { OTelCallMediaStreamTrackSpan } from "./OTelCallMediaStreamTrackSpan"; - -type TrackId = string; - -export abstract class OTelCallAbstractMediaStreamSpan { - protected readonly trackSpans = new Map< - TrackId, - OTelCallMediaStreamTrackSpan - >(); - public readonly span; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly callSpan: Span, - protected readonly type: string, - ) { - const ctx = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - callSpan, - ); - const options = { - links: [ - { - context: callSpan.spanContext(), - }, - ], - }; - this.span = oTel.tracer.startSpan(this.type, options, ctx); - } - - protected upsertTrackSpans(tracks: TrackStats[]): void { - let prvTracks: TrackId[] = [...this.trackSpans.keys()]; - tracks.forEach((t) => { - if (!this.trackSpans.has(t.id)) { - this.trackSpans.set( - t.id, - new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t), - ); - } - this.trackSpans.get(t.id)?.update(t); - prvTracks = prvTracks.filter((prvTrackId) => prvTrackId !== t.id); - }); - - prvTracks.forEach((prvTrackId) => { - this.trackSpans.get(prvTrackId)?.end(); - this.trackSpans.delete(prvTrackId); - }); - } - - public abstract update(data: object): void; - - public end(): void { - this.trackSpans.forEach((tSpan) => { - tSpan.end(); - }); - this.span.end(); - } -} diff --git a/src/otel/OTelCallFeedMediaStreamSpan.ts b/src/otel/OTelCallFeedMediaStreamSpan.ts deleted file mode 100644 index 59c780a5..00000000 --- a/src/otel/OTelCallFeedMediaStreamSpan.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type Span } from "@opentelemetry/api"; -import { - type CallFeedStats, - type TrackStats, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { type ElementCallOpenTelemetry } from "./otel"; -import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; - -export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { - private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean }; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly callSpan: Span, - callFeed: CallFeedStats, - ) { - const postFix = - callFeed.type === "local" && callFeed.prefix === "from-call-feed" - ? "(clone)" - : ""; - super(oTel, callSpan, `matrix.call.feed.${callFeed.type}${postFix}`); - this.span.setAttribute("feed.streamId", callFeed.stream); - this.span.setAttribute("feed.type", callFeed.type); - this.span.setAttribute("feed.readFrom", callFeed.prefix); - this.span.setAttribute("feed.purpose", callFeed.purpose); - this.prev = { - isAudioMuted: callFeed.isAudioMuted, - isVideoMuted: callFeed.isVideoMuted, - }; - this.span.addEvent("matrix.call.feed.initState", this.prev); - } - - public update(callFeed: CallFeedStats): void { - if (this.prev.isAudioMuted !== callFeed.isAudioMuted) { - this.span.addEvent("matrix.call.feed.audioMuted", { - isAudioMuted: callFeed.isAudioMuted, - }); - this.prev.isAudioMuted = callFeed.isAudioMuted; - } - if (this.prev.isVideoMuted !== callFeed.isVideoMuted) { - this.span.addEvent("matrix.call.feed.isVideoMuted", { - isVideoMuted: callFeed.isVideoMuted, - }); - this.prev.isVideoMuted = callFeed.isVideoMuted; - } - - const trackStats: TrackStats[] = []; - if (callFeed.video) { - trackStats.push(callFeed.video); - } - if (callFeed.audio) { - trackStats.push(callFeed.audio); - } - this.upsertTrackSpans(trackStats); - } -} diff --git a/src/otel/OTelCallMediaStreamTrackSpan.ts b/src/otel/OTelCallMediaStreamTrackSpan.ts deleted file mode 100644 index c81acd4f..00000000 --- a/src/otel/OTelCallMediaStreamTrackSpan.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type TrackStats } from "matrix-js-sdk/lib/webrtc/stats/statsReport"; -import opentelemetry, { type Span } from "@opentelemetry/api"; - -import { type ElementCallOpenTelemetry } from "./otel"; - -export class OTelCallMediaStreamTrackSpan { - private readonly span: Span; - private prev: TrackStats; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly streamSpan: Span, - data: TrackStats, - ) { - const ctx = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - streamSpan, - ); - const options = { - links: [ - { - context: streamSpan.spanContext(), - }, - ], - }; - const type = `matrix.call.track.${data.label}.${data.kind}`; - this.span = oTel.tracer.startSpan(type, options, ctx); - this.span.setAttribute("track.trackId", data.id); - this.span.setAttribute("track.kind", data.kind); - this.span.setAttribute("track.constrainDeviceId", data.constrainDeviceId); - this.span.setAttribute("track.settingDeviceId", data.settingDeviceId); - this.span.setAttribute("track.label", data.label); - - this.span.addEvent("matrix.call.track.initState", { - readyState: data.readyState, - muted: data.muted, - enabled: data.enabled, - }); - this.prev = data; - } - - public update(data: TrackStats): void { - if (this.prev.muted !== data.muted) { - this.span.addEvent("matrix.call.track.muted", { muted: data.muted }); - } - if (this.prev.enabled !== data.enabled) { - this.span.addEvent("matrix.call.track.enabled", { - enabled: data.enabled, - }); - } - if (this.prev.readyState !== data.readyState) { - this.span.addEvent("matrix.call.track.readyState", { - readyState: data.readyState, - }); - } - this.prev = data; - } - - public end(): void { - this.span.end(); - } -} diff --git a/src/otel/OTelCallTransceiverMediaStreamSpan.ts b/src/otel/OTelCallTransceiverMediaStreamSpan.ts deleted file mode 100644 index 675d793e..00000000 --- a/src/otel/OTelCallTransceiverMediaStreamSpan.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type Span } from "@opentelemetry/api"; -import { - type TrackStats, - type TransceiverStats, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { type ElementCallOpenTelemetry } from "./otel"; -import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; - -export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { - private readonly prev: { - direction: string; - currentDirection: string; - }; - - public constructor( - protected readonly oTel: ElementCallOpenTelemetry, - protected readonly callSpan: Span, - stats: TransceiverStats, - ) { - super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`); - this.span.setAttribute("transceiver.mid", stats.mid); - - this.prev = { - direction: stats.direction, - currentDirection: stats.currentDirection, - }; - this.span.addEvent("matrix.call.transceiver.initState", this.prev); - } - - public update(stats: TransceiverStats): void { - if (this.prev.currentDirection !== stats.currentDirection) { - this.span.addEvent("matrix.call.transceiver.currentDirection", { - currentDirection: stats.currentDirection, - }); - this.prev.currentDirection = stats.currentDirection; - } - if (this.prev.direction !== stats.direction) { - this.span.addEvent("matrix.call.transceiver.direction", { - direction: stats.direction, - }); - this.prev.direction = stats.direction; - } - - const trackStats: TrackStats[] = []; - if (stats.sender) { - trackStats.push(stats.sender); - } - if (stats.receiver) { - trackStats.push(stats.receiver); - } - this.upsertTrackSpans(trackStats); - } -} diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts deleted file mode 100644 index 668b989c..00000000 --- a/src/otel/OTelGroupCallMembership.ts +++ /dev/null @@ -1,477 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import opentelemetry, { - type Span, - type Attributes, - type Context, -} from "@opentelemetry/api"; -import { - type GroupCall, - type MatrixClient, - type MatrixEvent, - type RoomMember, -} from "matrix-js-sdk"; -import { logger } from "matrix-js-sdk/lib/logger"; -import { - type CallError, - type CallState, - type MatrixCall, - type VoipEvent, -} from "matrix-js-sdk/lib/webrtc/call"; -import { - type CallsByUserAndDevice, - type GroupCallError, - GroupCallEvent, - type GroupCallStatsReport, -} from "matrix-js-sdk/lib/webrtc/groupCall"; -import { - type ConnectionStatsReport, - type ByteSentStatsReport, - type SummaryStatsReport, - type CallFeedReport, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -import { ElementCallOpenTelemetry } from "./otel"; -import { ObjectFlattener } from "./ObjectFlattener"; -import { OTelCall } from "./OTelCall"; - -/** - * Represent the span of time which we intend to be joined to a group call - */ -export class OTelGroupCallMembership { - private callMembershipSpan?: Span; - private groupCallContext?: Context; - private myUserId = "unknown"; - private myDeviceId: string; - private myMember?: RoomMember; - private callsByCallId = new Map(); - private statsReportSpan: { - span: Span | undefined; - stats: OTelStatsReportEvent[]; - }; - private readonly speakingSpans = new Map>(); - - public constructor( - private groupCall: GroupCall, - client: MatrixClient, - ) { - const clientId = client.getUserId(); - if (clientId) { - this.myUserId = clientId; - const myMember = groupCall.room.getMember(clientId); - if (myMember) { - this.myMember = myMember; - } - } - this.myDeviceId = client.getDeviceId() || "unknown"; - this.statsReportSpan = { span: undefined, stats: [] }; - this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); - } - - public dispose(): void { - this.groupCall.removeListener( - GroupCallEvent.CallsChanged, - this.onCallsChanged, - ); - } - - public onJoinCall(): void { - if (!ElementCallOpenTelemetry.instance) return; - if (this.callMembershipSpan !== undefined) { - logger.warn("Call membership span is already started"); - return; - } - - // Create the main span that tracks the time we intend to be in the call - this.callMembershipSpan = - ElementCallOpenTelemetry.instance.tracer.startSpan( - "matrix.groupCallMembership", - ); - this.callMembershipSpan.setAttribute( - "matrix.confId", - this.groupCall.groupCallId, - ); - this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); - this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId); - this.callMembershipSpan.setAttribute( - "matrix.displayName", - this.myMember ? this.myMember.name : "unknown-name", - ); - - this.groupCallContext = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callMembershipSpan, - ); - - this.callMembershipSpan?.addEvent("matrix.joinCall"); - } - - public onLeaveCall(): void { - if (this.callMembershipSpan === undefined) { - logger.warn("Call membership span is already ended"); - return; - } - - this.callMembershipSpan.addEvent("matrix.leaveCall"); - // and end the span to indicate we've left - this.callMembershipSpan.end(); - this.callMembershipSpan = undefined; - this.groupCallContext = undefined; - } - - public onUpdateRoomState(event: MatrixEvent): void { - if ( - !event || - (!event.getType().startsWith("m.call") && - !event.getType().startsWith("org.matrix.msc3401.call")) - ) { - return; - } - - this.callMembershipSpan?.addEvent( - `matrix.roomStateEvent_${event.getType()}`, - ObjectFlattener.flattenVoipEvent(event.getContent()), - ); - } - - public onCallsChanged(calls: CallsByUserAndDevice): void { - for (const [userId, userCalls] of calls.entries()) { - for (const [deviceId, call] of userCalls.entries()) { - if (!this.callsByCallId.has(call.callId)) { - if (ElementCallOpenTelemetry.instance) { - const span = ElementCallOpenTelemetry.instance.tracer.startSpan( - `matrix.call`, - undefined, - this.groupCallContext, - ); - // XXX: anonymity - span.setAttribute("matrix.call.target.userId", userId); - span.setAttribute("matrix.call.target.deviceId", deviceId); - const displayName = - this.groupCall.room.getMember(userId)?.name ?? "unknown"; - span.setAttribute("matrix.call.target.displayName", displayName); - this.callsByCallId.set( - call.callId, - new OTelCall(userId, deviceId, call, span), - ); - } - } - } - } - - for (const callTrackingInfo of this.callsByCallId.values()) { - const userCalls = calls.get(callTrackingInfo.userId); - if ( - !userCalls || - !userCalls.has(callTrackingInfo.deviceId) || - userCalls.get(callTrackingInfo.deviceId)?.callId !== - callTrackingInfo.call.callId - ) { - callTrackingInfo.end(); - this.callsByCallId.delete(callTrackingInfo.call.callId); - } - } - } - - public onCallStateChange(call: MatrixCall, newState: CallState): void { - const callTrackingInfo = this.callsByCallId.get(call.callId); - if (!callTrackingInfo) { - logger.error(`Got call state change for unknown call ID ${call.callId}`); - return; - } - - callTrackingInfo.span.addEvent("matrix.call.stateChange", { - state: newState, - }); - } - - public onSendEvent(call: MatrixCall, event: VoipEvent): void { - const eventType = event.eventType as string; - if ( - !eventType.startsWith("m.call") && - !eventType.startsWith("org.matrix.call") - ) - return; - - const callTrackingInfo = this.callsByCallId.get(call.callId); - if (!callTrackingInfo) { - logger.error(`Got call send event for unknown call ID ${call.callId}`); - return; - } - - if (event.type === "toDevice") { - callTrackingInfo.span.addEvent( - `matrix.sendToDeviceEvent_${event.eventType}`, - ObjectFlattener.flattenVoipEvent(event), - ); - } else if (event.type === "sendEvent") { - callTrackingInfo.span.addEvent( - `matrix.sendToRoomEvent_${event.eventType}`, - ObjectFlattener.flattenVoipEvent(event), - ); - } - } - - public onReceivedVoipEvent(event: MatrixEvent): void { - // These come straight from CallEventHandler so don't have - // a call already associated (in principle we could receive - // events for calls we don't know about). - const callId = event.getContent().call_id; - if (!callId) { - this.callMembershipSpan?.addEvent("matrix.receive_voip_event_no_callid", { - "sender.userId": event.getSender(), - }); - logger.error("Received call event with no call ID!"); - return; - } - - const call = this.callsByCallId.get(callId); - if (!call) { - this.callMembershipSpan?.addEvent( - "matrix.receive_voip_event_unknown_callid", - { - "sender.userId": event.getSender(), - }, - ); - logger.error("Received call event for unknown call ID " + callId); - return; - } - - call.span.addEvent("matrix.receive_voip_event", { - "sender.userId": event.getSender(), - ...ObjectFlattener.flattenVoipEvent(event.getContent()), - }); - } - - public onToggleMicrophoneMuted(newValue: boolean): void { - this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { - "matrix.microphone.muted": newValue, - }); - } - - public onSetMicrophoneMuted(setMuted: boolean): void { - this.callMembershipSpan?.addEvent("matrix.setMicMuted", { - "matrix.microphone.muted": setMuted, - }); - } - - public onToggleLocalVideoMuted(newValue: boolean): void { - this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { - "matrix.video.muted": newValue, - }); - } - - public onSetLocalVideoMuted(setMuted: boolean): void { - this.callMembershipSpan?.addEvent("matrix.setVidMuted", { - "matrix.video.muted": setMuted, - }); - } - - public onToggleScreensharing(newValue: boolean): void { - this.callMembershipSpan?.addEvent("matrix.setVidMuted", { - "matrix.screensharing.enabled": newValue, - }); - } - - public onSpeaking( - member: RoomMember, - deviceId: string, - speaking: boolean, - ): void { - if (speaking) { - // Ensure that there's an audio activity span for this speaker - let deviceMap = this.speakingSpans.get(member); - if (deviceMap === undefined) { - deviceMap = new Map(); - this.speakingSpans.set(member, deviceMap); - } - - if (!deviceMap.has(deviceId)) { - const span = ElementCallOpenTelemetry.instance.tracer.startSpan( - "matrix.audioActivity", - undefined, - this.groupCallContext, - ); - span.setAttribute("matrix.userId", member.userId); - span.setAttribute("matrix.displayName", member.rawDisplayName); - - deviceMap.set(deviceId, span); - } - } else { - // End the audio activity span for this speaker, if any - const deviceMap = this.speakingSpans.get(member); - deviceMap?.get(deviceId)?.end(); - deviceMap?.delete(deviceId); - - if (deviceMap?.size === 0) this.speakingSpans.delete(member); - } - } - - public onCallError(error: CallError, call: MatrixCall): void { - const callTrackingInfo = this.callsByCallId.get(call.callId); - if (!callTrackingInfo) { - logger.error(`Got error for unknown call ID ${call.callId}`); - return; - } - - callTrackingInfo.span.recordException(error); - } - - public onGroupCallError(error: GroupCallError): void { - this.callMembershipSpan?.recordException(error); - } - - public onUndecryptableToDevice(event: MatrixEvent): void { - this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", { - "sender.userId": event.getSender(), - }); - } - - public onCallFeedStatsReport( - report: GroupCallStatsReport, - ): void { - if (!ElementCallOpenTelemetry.instance) return; - let call: OTelCall | undefined; - const callId = report.report?.callId; - - if (callId) { - call = this.callsByCallId.get(callId); - } - - if (!call) { - this.callMembershipSpan?.addEvent( - OTelStatsReportType.CallFeedReport + "_unknown_callId", - { - "call.callId": callId, - "call.opponentMemberId": report.report?.opponentMemberId - ? report.report?.opponentMemberId - : "unknown", - }, - ); - logger.error( - `Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`, - ); - return; - } else { - call.onCallFeedStats(report.report.callFeeds); - call.onTransceiverStats(report.report.transceiver); - } - } - - public onConnectionStatsReport( - statsReport: GroupCallStatsReport, - ): void { - this.buildCallStatsSpan( - OTelStatsReportType.ConnectionReport, - statsReport.report, - ); - } - - public onByteSentStatsReport( - statsReport: GroupCallStatsReport, - ): void { - this.buildCallStatsSpan( - OTelStatsReportType.ByteSentReport, - statsReport.report, - ); - } - - public buildCallStatsSpan( - type: OTelStatsReportType, - report: ByteSentStatsReport | ConnectionStatsReport, - ): void { - if (!ElementCallOpenTelemetry.instance) return; - let call: OTelCall | undefined; - const callId = report?.callId; - - if (callId) { - call = this.callsByCallId.get(callId); - } - - if (!call) { - this.callMembershipSpan?.addEvent(type + "_unknown_callid", { - "call.callId": callId, - "call.opponentMemberId": report.opponentMemberId - ? report.opponentMemberId - : "unknown", - }); - logger.error(`Received ${type} with unknown call ID: ${callId}`); - return; - } - const data = ObjectFlattener.flattenReportObject(type, report); - const ctx = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - call.span, - ); - - const options = { - links: [ - { - context: call.span.spanContext(), - }, - ], - }; - - const span = ElementCallOpenTelemetry.instance.tracer.startSpan( - type, - options, - ctx, - ); - - span.setAttribute("matrix.callId", callId ?? "unknown"); - span.setAttribute( - "matrix.opponentMemberId", - report.opponentMemberId ? report.opponentMemberId : "unknown", - ); - span.addEvent("matrix.call.connection_stats_event", data); - span.end(); - } - - public onSummaryStatsReport( - statsReport: GroupCallStatsReport, - ): void { - if (!ElementCallOpenTelemetry.instance) return; - - const type = OTelStatsReportType.SummaryReport; - const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport); - if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { - const ctx = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callMembershipSpan, - ); - const span = ElementCallOpenTelemetry.instance?.tracer.startSpan( - "matrix.groupCallMembership.summaryReport", - undefined, - ctx, - ); - if (span === undefined) { - return; - } - span.setAttribute("matrix.confId", this.groupCall.groupCallId); - span.setAttribute("matrix.userId", this.myUserId); - span.setAttribute( - "matrix.displayName", - this.myMember ? this.myMember.name : "unknown-name", - ); - span.addEvent(type, data); - span.end(); - } - } -} - -interface OTelStatsReportEvent { - type: OTelStatsReportType; - data: Attributes; -} - -enum OTelStatsReportType { - ConnectionReport = "matrix.call.stats.connection", - ByteSentReport = "matrix.call.stats.byteSent", - SummaryReport = "matrix.stats.summary", - CallFeedReport = "matrix.stats.call_feed", -} diff --git a/src/otel/ObjectFlattener.test.ts b/src/otel/ObjectFlattener.test.ts deleted file mode 100644 index 5685617c..00000000 --- a/src/otel/ObjectFlattener.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall"; -import { - type AudioConcealment, - type ByteSentStatsReport, - type ConnectionStatsReport, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; -import { describe, expect, it } from "vitest"; - -import { ObjectFlattener } from "../../src/otel/ObjectFlattener"; - -describe("ObjectFlattener", () => { - const noConcealment: AudioConcealment = { - concealedAudio: 0, - totalAudioDuration: 0, - }; - - const statsReport: GroupCallStatsReport = { - report: { - callId: "callId", - opponentMemberId: "opponentMemberId", - bandwidth: { upload: 426, download: 0 }, - bitrate: { - upload: 426, - download: 0, - audio: { - upload: 124, - download: 0, - }, - video: { - upload: 302, - download: 0, - }, - }, - packetLoss: { - total: 0, - download: 0, - upload: 0, - }, - framerate: { - local: new Map([ - ["LOCAL_AUDIO_TRACK_ID", 0], - ["LOCAL_VIDEO_TRACK_ID", 30], - ]), - remote: new Map([ - ["REMOTE_AUDIO_TRACK_ID", 0], - ["REMOTE_VIDEO_TRACK_ID", 60], - ]), - }, - resolution: { - local: new Map([ - ["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }], - ["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }], - ]), - remote: new Map([ - ["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }], - ["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }], - ]), - }, - jitter: new Map([ - ["REMOTE_AUDIO_TRACK_ID", 2], - ["REMOTE_VIDEO_TRACK_ID", 50], - ]), - codec: { - local: new Map([ - ["LOCAL_AUDIO_TRACK_ID", "opus"], - ["LOCAL_VIDEO_TRACK_ID", "v8"], - ]), - remote: new Map([ - ["REMOTE_AUDIO_TRACK_ID", "opus"], - ["REMOTE_VIDEO_TRACK_ID", "v9"], - ]), - }, - transport: [ - { - ip: "ff11::5fa:abcd:999c:c5c5:50000", - type: "udp", - localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - isFocus: true, - localCandidateType: "host", - remoteCandidateType: "host", - networkType: "ethernet", - rtt: NaN, - }, - { - ip: "10.10.10.2:22222", - type: "tcp", - localIp: "10.10.10.100:33333", - isFocus: true, - localCandidateType: "srfx", - remoteCandidateType: "srfx", - networkType: "ethernet", - rtt: 0, - }, - ], - audioConcealment: new Map([ - ["REMOTE_AUDIO_TRACK_ID", noConcealment], - ["REMOTE_VIDEO_TRACK_ID", noConcealment], - ]), - totalAudioConcealment: noConcealment, - }, - }; - - describe("on flattenObjectRecursive", () => { - it("should flatter an Map object", () => { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report.resolution, - flatObject, - "matrix.call.stats.connection.resolution.", - 0, - ); - expect(flatObject).toEqual({ - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width": - -1, - - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, - - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": - -1, - - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, - }); - }); - it("should flatter an Array object", () => { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report.transport, - flatObject, - "matrix.call.stats.connection.transport.", - 0, - ); - expect(flatObject).toEqual({ - "matrix.call.stats.connection.transport.0.ip": - "ff11::5fa:abcd:999c:c5c5:50000", - "matrix.call.stats.connection.transport.0.type": "udp", - "matrix.call.stats.connection.transport.0.localIp": - "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - "matrix.call.stats.connection.transport.0.isFocus": true, - "matrix.call.stats.connection.transport.0.localCandidateType": "host", - "matrix.call.stats.connection.transport.0.remoteCandidateType": "host", - "matrix.call.stats.connection.transport.0.networkType": "ethernet", - "matrix.call.stats.connection.transport.0.rtt": "NaN", - "matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222", - "matrix.call.stats.connection.transport.1.type": "tcp", - "matrix.call.stats.connection.transport.1.localIp": - "10.10.10.100:33333", - "matrix.call.stats.connection.transport.1.isFocus": true, - "matrix.call.stats.connection.transport.1.localCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.networkType": "ethernet", - "matrix.call.stats.connection.transport.1.rtt": 0, - }); - }); - }); - - describe("on flattenReportObject Connection Stats", () => { - it("should flatten a Report to otel Attributes Object", () => { - expect( - ObjectFlattener.flattenReportObject( - "matrix.call.stats.connection", - statsReport.report, - ), - ).toEqual({ - "matrix.call.stats.connection.callId": "callId", - "matrix.call.stats.connection.opponentMemberId": "opponentMemberId", - "matrix.call.stats.connection.bandwidth.download": 0, - "matrix.call.stats.connection.bandwidth.upload": 426, - "matrix.call.stats.connection.bitrate.audio.download": 0, - "matrix.call.stats.connection.bitrate.audio.upload": 124, - "matrix.call.stats.connection.bitrate.download": 0, - "matrix.call.stats.connection.bitrate.upload": 426, - "matrix.call.stats.connection.bitrate.video.download": 0, - "matrix.call.stats.connection.bitrate.video.upload": 302, - "matrix.call.stats.connection.codec.local.LOCAL_AUDIO_TRACK_ID": "opus", - "matrix.call.stats.connection.codec.local.LOCAL_VIDEO_TRACK_ID": "v8", - "matrix.call.stats.connection.codec.remote.REMOTE_AUDIO_TRACK_ID": - "opus", - "matrix.call.stats.connection.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9", - "matrix.call.stats.connection.framerate.local.LOCAL_AUDIO_TRACK_ID": 0, - "matrix.call.stats.connection.framerate.local.LOCAL_VIDEO_TRACK_ID": 30, - "matrix.call.stats.connection.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0, - "matrix.call.stats.connection.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60, - "matrix.call.stats.connection.jitter.REMOTE_AUDIO_TRACK_ID": 2, - "matrix.call.stats.connection.jitter.REMOTE_VIDEO_TRACK_ID": 50, - "matrix.call.stats.connection.packetLoss.download": 0, - "matrix.call.stats.connection.packetLoss.total": 0, - "matrix.call.stats.connection.packetLoss.upload": 0, - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width": - -1, - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, - "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": - -1, - "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": - -1, - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, - "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, - "matrix.call.stats.connection.transport.0.ip": - "ff11::5fa:abcd:999c:c5c5:50000", - "matrix.call.stats.connection.transport.0.type": "udp", - "matrix.call.stats.connection.transport.0.localIp": - "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - "matrix.call.stats.connection.transport.0.isFocus": true, - "matrix.call.stats.connection.transport.0.localCandidateType": "host", - "matrix.call.stats.connection.transport.0.remoteCandidateType": "host", - "matrix.call.stats.connection.transport.0.networkType": "ethernet", - "matrix.call.stats.connection.transport.0.rtt": "NaN", - "matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222", - "matrix.call.stats.connection.transport.1.type": "tcp", - "matrix.call.stats.connection.transport.1.localIp": - "10.10.10.100:33333", - "matrix.call.stats.connection.transport.1.isFocus": true, - "matrix.call.stats.connection.transport.1.localCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx", - "matrix.call.stats.connection.transport.1.networkType": "ethernet", - "matrix.call.stats.connection.transport.1.rtt": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0, - "matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0, - "matrix.call.stats.connection.totalAudioConcealment.concealedAudio": 0, - "matrix.call.stats.connection.totalAudioConcealment.totalAudioDuration": 0, - }); - }); - }); - - describe("on flattenByteSendStatsReportObject", () => { - const byteSentStatsReport = new Map< - string, - number - >() as ByteSentStatsReport; - byteSentStatsReport.callId = "callId"; - byteSentStatsReport.opponentMemberId = "opponentMemberId"; - byteSentStatsReport.set("4aa92608-04c6-428e-8312-93e17602a959", 132093); - byteSentStatsReport.set("a08e4237-ee30-4015-a932-b676aec894b1", 913448); - - it("should flatten a Report to otel Attributes Object", () => { - expect( - ObjectFlattener.flattenReportObject( - "matrix.call.stats.bytesSend", - byteSentStatsReport, - ), - ).toEqual({ - "matrix.call.stats.bytesSend.4aa92608-04c6-428e-8312-93e17602a959": 132093, - "matrix.call.stats.bytesSend.a08e4237-ee30-4015-a932-b676aec894b1": 913448, - }); - expect(byteSentStatsReport.callId).toEqual("callId"); - expect(byteSentStatsReport.opponentMemberId).toEqual("opponentMemberId"); - }); - }); -}); diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts deleted file mode 100644 index a963c743..00000000 --- a/src/otel/ObjectFlattener.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ -import { type Attributes } from "@opentelemetry/api"; -import { type VoipEvent } from "matrix-js-sdk/lib/webrtc/call"; -import { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall"; -import { - type ByteSentStatsReport, - type ConnectionStatsReport, - type SummaryStatsReport, -} from "matrix-js-sdk/lib/webrtc/stats/statsReport"; - -export class ObjectFlattener { - public static flattenReportObject( - prefix: string, - report: ConnectionStatsReport | ByteSentStatsReport, - ): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0); - return flatObject; - } - - public static flattenByteSentStatsReportObject( - statsReport: GroupCallStatsReport, - ): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report, - flatObject, - "matrix.stats.bytesSent.", - 0, - ); - return flatObject; - } - - public static flattenSummaryStatsReportObject( - statsReport: GroupCallStatsReport, - ): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report, - flatObject, - "matrix.stats.summary.", - 0, - ); - return flatObject; - } - - /* Flattens out an object into a single layer with components - * of the key separated by dots - */ - public static flattenVoipEvent(event: VoipEvent): Attributes { - const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - event as unknown as Record, // XXX Types - flatObject, - "matrix.event.", - 0, - ); - - return flatObject; - } - - public static flattenObjectRecursive( - obj: object, - flatObject: Attributes, - prefix: string, - depth: number, - ): void { - if (depth > 10) - throw new Error( - "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + - prefix, - ); - let entries; - if (obj instanceof Map) { - entries = obj.entries(); - } else { - entries = Object.entries(obj); - } - for (const [k, v] of entries) { - if (["string", "number", "boolean"].includes(typeof v) || v === null) { - let value; - value = v === null ? "null" : v; - value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value; - flatObject[prefix + k] = value; - } else if (typeof v === "object") { - ObjectFlattener.flattenObjectRecursive( - v, - flatObject, - prefix + k + ".", - depth + 1, - ); - } - } - } -} diff --git a/src/otel/otel.test.ts b/src/otel/otel.test.ts deleted file mode 100644 index 0bf0573f..00000000 --- a/src/otel/otel.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { - expect, - describe, - it, - vi, - beforeEach, - beforeAll, - afterAll, -} from "vitest"; - -import { ElementCallOpenTelemetry } from "./otel"; -import { mockConfig } from "../utils/test"; - -describe("ElementCallOpenTelemetry", () => { - describe("embedded package", () => { - beforeAll(() => { - vi.stubEnv("VITE_PACKAGE", "embedded"); - }); - - beforeEach(() => { - mockConfig({}); - }); - - afterAll(() => { - vi.unstubAllEnvs(); - }); - - it("does not create instance without config value", () => { - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false); - }); - - it("ignores config value and does not create instance", () => { - mockConfig({ - opentelemetry: { - collector_url: "https://collector.example.com.localhost", - }, - }); - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false); - }); - }); - - describe("full package", () => { - beforeAll(() => { - vi.stubEnv("VITE_PACKAGE", "full"); - }); - - beforeEach(() => { - mockConfig({}); - }); - - afterAll(() => { - vi.unstubAllEnvs(); - }); - - it("does not create instance without config value", () => { - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false); - }); - - it("creates instance with config value", () => { - mockConfig({ - opentelemetry: { - collector_url: "https://collector.example.com.localhost", - }, - }); - ElementCallOpenTelemetry.globalInit(); - expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(true); - }); - }); -}); diff --git a/src/otel/otel.ts b/src/otel/otel.ts deleted file mode 100644 index 915c3d58..00000000 --- a/src/otel/otel.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { - SimpleSpanProcessor, - type SpanProcessor, -} from "@opentelemetry/sdk-trace-base"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; -import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import opentelemetry, { type Tracer } from "@opentelemetry/api"; -import { resourceFromAttributes } from "@opentelemetry/resources"; -import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; -import { logger } from "matrix-js-sdk/lib/logger"; - -import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor"; -import { Config } from "../config/Config"; -import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor"; -import { getRageshakeSubmitUrl } from "../settings/submit-rageshake"; - -const SERVICE_NAME = "element-call"; - -let sharedInstance: ElementCallOpenTelemetry; - -export class ElementCallOpenTelemetry { - private _provider: WebTracerProvider; - private _tracer: Tracer; - private otlpExporter?: OTLPTraceExporter; - public readonly rageshakeProcessor?: RageshakeSpanProcessor; - - public static globalInit(): void { - // this is only supported in the full package as the is currently no support for passing in the collector URL from the widget host - const collectorUrl = - import.meta.env.VITE_PACKAGE === "full" - ? Config.get().opentelemetry?.collector_url - : undefined; - // we always enable opentelemetry in general. We only enable the OTLP - // collector if a URL is defined (and in future if another setting is defined) - // Posthog reporting is enabled or disabled - // within the posthog code. - const shouldEnableOtlp = Boolean(collectorUrl); - - if (!sharedInstance || sharedInstance.isOtlpEnabled !== shouldEnableOtlp) { - logger.info("(Re)starting OpenTelemetry debug reporting"); - sharedInstance?.dispose(); - - sharedInstance = new ElementCallOpenTelemetry( - collectorUrl, - getRageshakeSubmitUrl(), - ); - } - } - - public static get instance(): ElementCallOpenTelemetry { - return sharedInstance; - } - - private constructor( - collectorUrl: string | undefined, - rageshakeUrl: string | undefined, - ) { - const spanProcessors: SpanProcessor[] = []; - - if (collectorUrl) { - logger.info("Enabling OTLP collector with URL " + collectorUrl); - this.otlpExporter = new OTLPTraceExporter({ - url: collectorUrl, - }); - spanProcessors.push(new SimpleSpanProcessor(this.otlpExporter)); - } else { - logger.info("OTLP collector disabled"); - } - - if (rageshakeUrl) { - this.rageshakeProcessor = new RageshakeSpanProcessor(); - spanProcessors.push(this.rageshakeProcessor); - } - - spanProcessors.push(new PosthogSpanProcessor()); - - this._provider = new WebTracerProvider({ - resource: resourceFromAttributes({ - // This is how we can make Jaeger show a reasonable service in the dropdown on the left. - [ATTR_SERVICE_NAME]: SERVICE_NAME, - }), - spanProcessors, - }); - - opentelemetry.trace.setGlobalTracerProvider(this._provider); - this._tracer = opentelemetry.trace.getTracer( - // This is not the serviceName shown in jaeger - "my-element-call-otl-tracer", - ); - } - - public dispose(): void { - opentelemetry.trace.disable(); - this._provider?.shutdown().catch((e) => { - logger.error("Failed to shutdown OpenTelemetry", e); - }); - } - - public get isOtlpEnabled(): boolean { - return Boolean(this.otlpExporter); - } - - public get tracer(): Tracer { - return this._tracer; - } - - public get provider(): WebTracerProvider { - return this._provider; - } -} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b17d3aae..72a35da6 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -48,7 +48,6 @@ import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { ElementWidgetActions, widget } from "../widget"; import styles from "./InCallView.module.css"; import { GridTile } from "../tile/GridTile"; -import { type OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; @@ -180,7 +179,6 @@ export interface InCallViewProps { matrixRoom: MatrixRoom; muteStates: MuteStates; header: HeaderStyle; - otelGroupCallMembership?: OTelGroupCallMembership; onShareClick: (() => void) | null; } diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index bfd55126..276d8e60 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -17,7 +17,6 @@ import { type CryptoApi } from "matrix-js-sdk/lib/crypto-api"; import { getLogsForReport } from "./rageshake"; import { useClient } from "../ClientContext"; import { Config } from "../config/Config"; -import { ElementCallOpenTelemetry } from "../otel/otel"; import { type RageshakeRequestModal } from "../room/RageshakeRequestModal"; import { getUrlParams } from "../UrlParams"; @@ -274,14 +273,6 @@ export function useSubmitRageshake( for (const entry of logs) { body.append("compressed-log", await gzip(entry.lines), entry.id); } - - body.append( - "file", - await gzip( - ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump(), - ), - "traces.json.gz", - ); } if (opts.rageshakeRequestId) { From 2f928673831fd2730a1bb150c73e82e9433c372e Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 24 Nov 2025 12:36:36 +0100 Subject: [PATCH 02/76] remove otel deps --- package.json | 7 -- yarn.lock | 249 +-------------------------------------------------- 2 files changed, 1 insertion(+), 255 deletions(-) diff --git a/package.json b/package.json index 62ea9f4f..75ae74e5 100644 --- a/package.json +++ b/package.json @@ -47,13 +47,6 @@ "@livekit/protocol": "^1.42.2", "@livekit/track-processors": "^0.5.5", "@mediapipe/tasks-vision": "^0.10.18", - "@opentelemetry/api": "^1.4.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/sdk-trace-web": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.25.1", "@playwright/test": "^1.56.1", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-slider": "^1.1.2", diff --git a/yarn.lock b/yarn.lock index 97ca1985..f7b4242e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2989,146 +2989,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/api-logs@npm:0.203.0" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/e7a0a0ff46aaeb62192a99f45ef4889222e4fea09be25cab6fea811afc2df95c02ea050b2c98dfc0fc5a6ec6a623d87096af2751fdf91ddbb3afcab61b5325da - languageName: node - linkType: hard - -"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0": - version: 1.9.0 - resolution: "@opentelemetry/api@npm:1.9.0" - checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add - languageName: node - linkType: hard - -"@opentelemetry/core@npm:2.0.1, @opentelemetry/core@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/core@npm:2.0.1" - dependencies: - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8 - languageName: node - linkType: hard - -"@opentelemetry/exporter-trace-otlp-http@npm:^0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.203.0" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-exporter-base": "npm:0.203.0" - "@opentelemetry/otlp-transformer": "npm:0.203.0" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/21a65ebc40dcab05cf11178e5037f96847ce344c4a855aac46dcab3f74982016318ee75fafdfeeb42f10b92a0a781b7cd8b2b5b036cbe53c14714fd13940142e - languageName: node - linkType: hard - -"@opentelemetry/otlp-exporter-base@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.203.0" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-transformer": "npm:0.203.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/ad5b771b06b192f06f332f60701d1ad208df88a05975b16e1cdd1dff8e1cb66e775b3e9de513c2f5d48f390f25ca35411ead08ce4849c8203b86a264d34561d3 - languageName: node - linkType: hard - -"@opentelemetry/otlp-transformer@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.203.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.203.0" - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-logs": "npm:0.203.0" - "@opentelemetry/sdk-metrics": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" - protobufjs: "npm:^7.3.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/3f7b4bfe4bcab4db434ff2c4e59b53de53642d379b80056610456d8e9ae0cbab0f8b69f088078637b7b5ceffd0ac2fda68469c5f295b1c0ac625f522f640338c - languageName: node - linkType: hard - -"@opentelemetry/resources@npm:2.0.1, @opentelemetry/resources@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/resources@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341 - languageName: node - linkType: hard - -"@opentelemetry/sdk-logs@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/sdk-logs@npm:0.203.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.203.0" - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/02dd9d9969628f05f71ae1d149f1aa6d1fee2dad607923a68a1cfc923e94b046dcc0e18e85e865324e3bda0cee7a5a0ba9fa0d57e4e95fa672be103e2ce60270 - languageName: node - linkType: hard - -"@opentelemetry/sdk-metrics@npm:2.0.1": - version: 2.0.1 - resolution: "@opentelemetry/sdk-metrics@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ">=1.9.0 <1.10.0" - checksum: 10c0/fcf7ae23d459e5da7cb6fe150064b6dc4e11e47925b08980c3b357bd5534ad388898bbacd0ff8befef6801f43b35142dc7123f028ffde2d0fe2bd72177d07639 - languageName: node - linkType: hard - -"@opentelemetry/sdk-trace-base@npm:2.0.1, @opentelemetry/sdk-trace-base@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/sdk-trace-base@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b - languageName: node - linkType: hard - -"@opentelemetry/sdk-trace-web@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/sdk-trace-web@npm:2.0.1" - dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/48821b91430e24378b0b5b2632e78efdd018a3f840462a6aeba6ce318a6480bad2f623cc7f7f625a9266028ad44b78eb8456181778de6cb18725f26c44e2729b - languageName: node - linkType: hard - -"@opentelemetry/semantic-conventions@npm:^1.25.1, @opentelemetry/semantic-conventions@npm:^1.29.0": - version: 1.36.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.36.0" - checksum: 10c0/edc8a6fe3ec4fc0c67ba3a92b86fb3dcc78fe1eb4f19838d8013c3232b9868540a034dd25cfe0afdd5eae752c5f0e9f42272ff46da144a2d5b35c644478e1c62 - languageName: node - linkType: hard - "@oxc-resolver/binding-darwin-arm64@npm:11.3.0": version: 11.3.0 resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.3.0" @@ -3384,79 +3244,6 @@ __metadata: languageName: node linkType: hard -"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/aspromise@npm:1.1.2" - checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f - languageName: node - linkType: hard - -"@protobufjs/base64@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/base64@npm:1.1.2" - checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6 - languageName: node - linkType: hard - -"@protobufjs/codegen@npm:^2.0.4": - version: 2.0.4 - resolution: "@protobufjs/codegen@npm:2.0.4" - checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43 - languageName: node - linkType: hard - -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70 - languageName: node - linkType: hard - -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.1" - "@protobufjs/inquire": "npm:^1.1.0" - checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233 - languageName: node - linkType: hard - -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069 - languageName: node - linkType: hard - -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a - languageName: node - linkType: hard - -"@protobufjs/path@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/path@npm:1.1.2" - checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4 - languageName: node - linkType: hard - -"@protobufjs/pool@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/pool@npm:1.1.0" - checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038 - languageName: node - linkType: hard - -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487 - languageName: node - linkType: hard - "@radix-ui/number@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/number@npm:1.1.1" @@ -5240,7 +5027,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0": +"@types/node@npm:*": version: 24.1.0 resolution: "@types/node@npm:24.1.0" dependencies: @@ -7485,13 +7272,6 @@ __metadata: "@livekit/protocol": "npm:^1.42.2" "@livekit/track-processors": "npm:^0.5.5" "@mediapipe/tasks-vision": "npm:^0.10.18" - "@opentelemetry/api": "npm:^1.4.0" - "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:^0.203.0" - "@opentelemetry/resources": "npm:^2.0.0" - "@opentelemetry/sdk-trace-base": "npm:^2.0.0" - "@opentelemetry/sdk-trace-web": "npm:^2.0.0" - "@opentelemetry/semantic-conventions": "npm:^1.25.1" "@playwright/test": "npm:^1.56.1" "@radix-ui/react-dialog": "npm:^1.0.4" "@radix-ui/react-slider": "npm:^1.1.2" @@ -10198,13 +9978,6 @@ __metadata: languageName: node linkType: hard -"long@npm:^5.0.0": - version: 5.3.1 - resolution: "long@npm:5.3.1" - checksum: 10c0/8726994c6359bb7162fb94563e14c3f9c0f0eeafd90ec654738f4f144a5705756d36a873c442f172ee2a4b51e08d14ab99765b49aa1fb994c5ba7fe12057bca2 - languageName: node - linkType: hard - "loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -11718,26 +11491,6 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^7.3.0": - version: 7.4.0 - resolution: "protobufjs@npm:7.4.0" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.2" - "@protobufjs/base64": "npm:^1.1.2" - "@protobufjs/codegen": "npm:^2.0.4" - "@protobufjs/eventemitter": "npm:^1.1.0" - "@protobufjs/fetch": "npm:^1.1.0" - "@protobufjs/float": "npm:^1.0.2" - "@protobufjs/inquire": "npm:^1.1.0" - "@protobufjs/path": "npm:^1.1.2" - "@protobufjs/pool": "npm:^1.1.0" - "@protobufjs/utf8": "npm:^1.1.0" - "@types/node": "npm:>=13.7.0" - long: "npm:^5.0.0" - checksum: 10c0/a5460a63fe596523b9a067cbce39a6b310d1a71750fda261f076535662aada97c24450e18c5bc98a27784f70500615904ff1227e1742183509f0db4fdede669b - languageName: node - linkType: hard - "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" From 8bf651837139184c107582c5d74c135934455035 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 24 Nov 2025 12:42:00 +0100 Subject: [PATCH 03/76] remove span processor endpoints --- src/analytics/PosthogSpanProcessor.ts | 157 ------------------------ src/analytics/RageshakeSpanProcessor.ts | 135 -------------------- 2 files changed, 292 deletions(-) delete mode 100644 src/analytics/PosthogSpanProcessor.ts delete mode 100644 src/analytics/RageshakeSpanProcessor.ts diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts deleted file mode 100644 index a0046200..00000000 --- a/src/analytics/PosthogSpanProcessor.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { - type SpanProcessor, - type ReadableSpan, - type Span, -} from "@opentelemetry/sdk-trace-base"; -import { hrTimeToMilliseconds } from "@opentelemetry/core"; -import { logger } from "matrix-js-sdk/lib/logger"; - -import { PosthogAnalytics } from "./PosthogAnalytics"; - -interface PrevCall { - callId: string; - hangupTs: number; -} - -/** - * The maximum time between hanging up and joining the same call that we would - * consider a 'rejoin' on the user's part. - */ -const maxRejoinMs = 2 * 60 * 1000; // 2 minutes - -/** - * Span processor that extracts certain metrics from spans to send to PostHog - */ -export class PosthogSpanProcessor implements SpanProcessor { - public async forceFlush(): Promise {} - - public onStart(span: Span): void { - // Hack: Yield to allow attributes to be set before processing - try { - switch (span.name) { - case "matrix.groupCallMembership": - this.onGroupCallMembershipStart(span); - return; - case "matrix.groupCallMembership.summaryReport": - this.onSummaryReportStart(span); - return; - } - } catch (e) { - // log to avoid tripping @typescript-eslint/no-unused-vars - logger.debug(e); - } - } - - public onEnd(span: ReadableSpan): void { - switch (span.name) { - case "matrix.groupCallMembership": - this.onGroupCallMembershipEnd(span); - return; - } - } - - private get prevCall(): PrevCall | null { - // This is stored in localStorage so we can remember the previous call - // across app restarts - const data = localStorage.getItem("matrix-prev-call"); - if (data === null) return null; - - try { - return JSON.parse(data); - } catch (e) { - logger.warn("Invalid prev call data", data, "error:", e); - return null; - } - } - - private set prevCall(data: PrevCall | null) { - localStorage.setItem("matrix-prev-call", JSON.stringify(data)); - } - - private onGroupCallMembershipStart(span: ReadableSpan): void { - const prevCall = this.prevCall; - const newCallId = span.attributes["matrix.confId"] as string; - - // If the user joined the same call within a short time frame, log this as a - // rejoin. This is interesting as a call quality metric, since rejoins may - // indicate that users had to intervene to make the product work. - if (prevCall !== null && newCallId === prevCall.callId) { - const duration = hrTimeToMilliseconds(span.startTime) - prevCall.hangupTs; - if (duration <= maxRejoinMs) { - PosthogAnalytics.instance.trackEvent({ - eventName: "Rejoin", - callId: prevCall.callId, - rejoinDuration: duration, - }); - } - } - } - - private onGroupCallMembershipEnd(span: ReadableSpan): void { - this.prevCall = { - callId: span.attributes["matrix.confId"] as string, - hangupTs: hrTimeToMilliseconds(span.endTime), - }; - } - - private onSummaryReportStart(span: ReadableSpan): void { - // Searching for an event like this: - // matrix.stats.summary - // matrix.stats.summary.percentageReceivedAudioMedia: 0.75 - // matrix.stats.summary.percentageReceivedMedia: 1 - // matrix.stats.summary.percentageReceivedVideoMedia: 0.75 - // matrix.stats.summary.maxJitter: 100 - // matrix.stats.summary.maxPacketLoss: 20 - const event = span.events.find((e) => e.name === "matrix.stats.summary"); - if (event !== undefined) { - const attributes = event.attributes; - if (attributes) { - const mediaReceived = `${attributes["matrix.stats.summary.percentageReceivedMedia"]}`; - const videoReceived = `${attributes["matrix.stats.summary.percentageReceivedVideoMedia"]}`; - const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`; - const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`; - const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; - const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`; - const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`; - const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`; - const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`; - const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`; - const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`; - - PosthogAnalytics.instance.trackEvent( - { - eventName: "MediaReceived", - callId: span.attributes["matrix.confId"] as string, - mediaReceived: mediaReceived, - audioReceived: audioReceived, - videoReceived: videoReceived, - maxJitter: maxJitter, - maxPacketLoss: maxPacketLoss, - peerConnections: peerConnections, - percentageConcealedAudio: percentageConcealedAudio, - opponentUsersInCall: opponentUsersInCall, - opponentDevicesInCall: opponentDevicesInCall, - diffDevicesToPeerConnections: diffDevicesToPeerConnections, - ratioPeerConnectionToDevices: ratioPeerConnectionToDevices, - }, - // Send instantly because the window might be closing - { send_instantly: true }, - ); - } - } - } - - /** - * Shutdown the processor. - */ - public async shutdown(): Promise { - return Promise.resolve(); - } -} diff --git a/src/analytics/RageshakeSpanProcessor.ts b/src/analytics/RageshakeSpanProcessor.ts deleted file mode 100644 index eca657db..00000000 --- a/src/analytics/RageshakeSpanProcessor.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type AttributeValue, type Attributes } from "@opentelemetry/api"; -import { hrTimeToMicroseconds } from "@opentelemetry/core"; -import { - type SpanProcessor, - type ReadableSpan, - type Span, -} from "@opentelemetry/sdk-trace-base"; - -const dumpAttributes = ( - attr: Attributes, -): { - key: string; - type: - | "string" - | "number" - | "bigint" - | "boolean" - | "symbol" - | "undefined" - | "object" - | "function"; - value: AttributeValue | undefined; -}[] => - Object.entries(attr).map(([key, value]) => ({ - key, - type: typeof value, - value, - })); - -/** - * Exports spans on demand to the Jaeger JSON format, which can be attached to - * rageshakes and loaded into analysis tools like Jaeger and Stalk. - */ -export class RageshakeSpanProcessor implements SpanProcessor { - private readonly spans: ReadableSpan[] = []; - - public async forceFlush(): Promise {} - - public onStart(span: Span): void { - this.spans.push(span); - } - - public onEnd(): void {} - - /** - * Dumps the spans collected so far as Jaeger-compatible JSON. - */ - public dump(): string { - const now = Date.now() * 1000; // Jaeger works in microseconds - const traces = new Map(); - - // Organize spans by their trace IDs - for (const span of this.spans) { - const traceId = span.spanContext().traceId; - let trace = traces.get(traceId); - - if (trace === undefined) { - trace = []; - traces.set(traceId, trace); - } - - trace.push(span); - } - - const processId = "p1"; - const processes = { - [processId]: { - serviceName: "element-call", - tags: [], - }, - warnings: null, - }; - - return JSON.stringify({ - // Honestly not sure what some of these fields mean, I just know that - // they're present in Jaeger JSON exports - total: 0, - limit: 0, - offset: 0, - errors: null, - data: [...traces.entries()].map(([traceId, spans]) => ({ - traceID: traceId, - warnings: null, - processes, - spans: spans.map((span) => { - const ctx = span.spanContext(); - const startTime = hrTimeToMicroseconds(span.startTime); - // If the span has not yet ended, pretend that it ends now - const duration = - span.duration[0] === -1 - ? now - startTime - : hrTimeToMicroseconds(span.duration); - - return { - traceID: traceId, - spanID: ctx.spanId, - operationName: span.name, - processID: processId, - warnings: null, - startTime, - duration, - references: - span.parentSpanContext?.spanId === undefined - ? [] - : [ - { - refType: "CHILD_OF", - traceID: traceId, - spanID: span.parentSpanContext?.spanId, - }, - ], - tags: dumpAttributes(span.attributes), - logs: span.events.map((event) => ({ - timestamp: hrTimeToMicroseconds(event.time), - // The name of the event is in the "event" field, aparently. - fields: [ - ...dumpAttributes(event.attributes ?? {}), - { key: "event", type: "string", value: event.name }, - ], - })), - }; - }), - })), - }); - } - - public async shutdown(): Promise {} -} From 909d980dff83d96bd81db2d692b9c4f9f31745c9 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 15 Dec 2025 18:23:30 +0100 Subject: [PATCH 04/76] still with broken tests... --- package.json | 2 +- src/e2ee/matrixKeyProvider.ts | 65 ++- src/livekit/MatrixAudioRenderer.tsx | 3 +- src/room/InCallView.tsx | 1 + src/state/CallViewModel/CallViewModel.test.ts | 3 - src/state/CallViewModel/CallViewModel.ts | 25 +- .../remoteMembers/ConnectionManager.ts | 25 +- .../MatrixLivekitMembers.test.ts | 437 ++++++++---------- .../remoteMembers/MatrixLivekitMembers.ts | 107 ++++- yarn.lock | 42 +- 10 files changed, 353 insertions(+), 357 deletions(-) diff --git a/package.json b/package.json index 21c870ad..1efb504b 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "^39.2.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00", "matrix-widget-api": "^1.14.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index 95033f87..1d1c4588 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -11,6 +11,12 @@ import { type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; + +import { + computeLivekitParticipantIdentity, + livekitIdentityInput, +} from "../state/CallViewModel/remoteMembers/MatrixLivekitMembers"; export class MatrixKeyProvider extends BaseKeyProvider { private rtcSession?: MatrixRTCSession; @@ -42,31 +48,46 @@ export class MatrixKeyProvider extends BaseKeyProvider { private onEncryptionKeyChanged = ( encryptionKey: Uint8Array, encryptionKeyIndex: number, - participantId: string, + membership: CallMembershipIdentityParts, ): void => { - crypto.subtle - .importKey("raw", encryptionKey, "HKDF", false, [ + const unhashedIdentity = livekitIdentityInput(membership); + + // This is the only way we can get the kind of the membership event we just received the key for. + // best case we want to recompute this once the memberships change (you can receive the key before the participant...) + // + // TODO change this to `?? "rtc"` for newer versions. + const kind = + this.rtcSession?.memberships.find( + (m) => + m.userId === membership.userId && + m.deviceId === membership.deviceId && + m.memberId === membership.memberId, + )?.kind ?? "session"; + + Promise.all([ + crypto.subtle.importKey("raw", encryptionKey, "HKDF", false, [ "deriveBits", "deriveKey", - ]) - .then( - (keyMaterial) => { - this.onSetEncryptionKey( - keyMaterial, - participantId, - encryptionKeyIndex, - ); + ]), + computeLivekitParticipantIdentity(membership, kind), + ]).then( + ([keyMaterial, livekitParticipantId]) => { + this.onSetEncryptionKey( + keyMaterial, + livekitParticipantId, + encryptionKeyIndex, + ); - logger.debug( - `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex}`, - ); - }, - (e) => { - logger.error( - `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex}`, - e, - ); - }, - ); + logger.debug( + `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${livekitParticipantId} (before hash: ${unhashedIdentity}) encryptionKeyIndex=${encryptionKeyIndex}`, + ); + }, + (e) => { + logger.error( + `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${unhashedIdentity} encryptionKeyIndex=${encryptionKeyIndex}`, + e, + ); + }, + ); }; } diff --git a/src/livekit/MatrixAudioRenderer.tsx b/src/livekit/MatrixAudioRenderer.tsx index 5b1149e9..0fa5d000 100644 --- a/src/livekit/MatrixAudioRenderer.tsx +++ b/src/livekit/MatrixAudioRenderer.tsx @@ -15,7 +15,6 @@ import { type AudioTrackProps, } from "@livekit/components-react"; import { logger } from "matrix-js-sdk/lib/logger"; -import { type ParticipantId } from "matrix-js-sdk/lib/matrixrtc"; import { useEarpieceAudioConfig } from "../MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; @@ -32,7 +31,7 @@ export interface MatrixAudioRendererProps { * This list needs to be composed based on the matrixRTC members so that we do not play audio from users * that are not expected to be in the rtc session (local user is excluded). */ - validIdentities: ParticipantId[]; + validIdentities: string[]; /** * If set to `true`, mutes all audio tracks rendered by the component. * @remarks diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 6ae004d8..e9932fdc 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -785,6 +785,7 @@ export const InCallView: FC = ({ onTouchEnd={onControlsTouchEnd} /> )} + {!showControls &&
}
); diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 2e5b5700..be598702 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -1248,9 +1248,6 @@ describe("CallViewModel", () => { y: () => { rtcSession.membershipStatus = Status.Connected; }, - n: () => { - rtcSession.membershipStatus = Status.Reconnecting; - }, }); schedule(probablyLeftMarbles, { y: () => { diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 5cc33f5d..289b642c 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -591,10 +591,9 @@ export function createCallViewModel$( const audioParticipants$ = scope.behavior( matrixLivekitMembers$.pipe( - switchMap((membersWithEpoch) => { - const members = membersWithEpoch.value; + switchMap((members) => { const a$ = combineLatest( - members.map((member) => + members.value.map((member) => combineLatest([member.connection$, member.participant$]).pipe( map(([connection, participant]) => { // do not render audio for local participant @@ -667,22 +666,22 @@ export function createCallViewModel$( generateItems( function* ([ localMatrixLivekitMember, - { value: matrixLivekitMembers }, + matrixLivekitMembers, duplicateTiles, ]) { - let localParticipantId: string | undefined = undefined; + let localUserMediaId: string | undefined = undefined; // add local member if available if (localMatrixLivekitMember) { const { userId, participant$, connection$, membership$ } = localMatrixLivekitMember; - localParticipantId = `${userId}:${membership$.value.deviceId}`; // should be membership$.value.membershipID which is not optional - // const participantId = membership$.value.membershipID; - if (localParticipantId) { + localUserMediaId = `${userId}:${membership$.value.deviceId}`; // should be membership$.value.membershipID which is not optional + + if (localUserMediaId) { for (let dup = 0; dup < 1 + duplicateTiles; dup++) { yield { keys: [ dup, - localParticipantId, + localUserMediaId, userId, participant$, connection$, @@ -698,13 +697,13 @@ export function createCallViewModel$( participant$, connection$, membership$, - } of matrixLivekitMembers) { - const participantId = `${userId}:${membership$.value.deviceId}`; - if (participantId === localParticipantId) continue; + } of matrixLivekitMembers.value) { + const userMediaId = `${userId}:${membership$.value.deviceId}`; + if (userMediaId === localUserMediaId) continue; // const participantId = membership$.value?.identity; for (let dup = 0; dup < 1 + duplicateTiles; dup++) { yield { - keys: [dup, participantId, userId, participant$, connection$], + keys: [dup, userMediaId, userId, participant$, connection$], data: undefined, }; } diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index 755ba3dd..6660df62 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -6,10 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { - type LivekitTransport, - type ParticipantId, -} from "matrix-js-sdk/lib/matrixrtc"; +import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { combineLatest, map, of, switchMap, tap } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; @@ -62,24 +59,8 @@ export class ConnectionManagerData { const key = transport.livekit_service_url + "|" + transport.livekit_alias; return this.store.get(key)?.[1] ?? []; } - /** - * Get all connections where the given participant is publishing. - * In theory, there could be several connections where the same participant is publishing but with - * only well behaving clients a participant should only be publishing on a single connection. - * @param participantId - */ - public getConnectionsForParticipant( - participantId: ParticipantId, - ): Connection[] { - const connections: Connection[] = []; - for (const [connection, participants] of this.store.values()) { - if (participants.some((p) => p.identity === participantId)) { - connections.push(connection); - } - } - return connections; - } } + interface Props { scope: ObservableScope; connectionFactory: ConnectionFactory; @@ -202,7 +183,7 @@ export function createConnectionManager$({ ); }), ), - new Epoch(new ConnectionManagerData()), + new Epoch(new ConnectionManagerData(), -1), ); return { connectionManagerData$ }; diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts index e675f723..c2e60798 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts @@ -10,8 +10,7 @@ import { type CallMembership, type LivekitTransport, } from "matrix-js-sdk/lib/matrixrtc"; -import { getParticipantId } from "matrix-js-sdk/lib/matrixrtc/utils"; -import { combineLatest, map, type Observable } from "rxjs"; +import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs"; import { type IConnectionManager } from "./ConnectionManager.ts"; import { @@ -26,14 +25,19 @@ import { } from "../../ObservableScope.ts"; import { ConnectionManagerData } from "./ConnectionManager.ts"; import { + flushPromises, mockCallMembership, mockRemoteParticipant, withTestScheduler, } from "../../../utils/test.ts"; import { type Connection } from "./Connection.ts"; +import { constant } from "../../Behavior.ts"; let testScope: ObservableScope; +const fallbackMemberId = (userId: string, deviceId: string): string => + `${userId}:${deviceId}`; + const transportA: LivekitTransport = { type: "livekit", livekit_service_url: "https://lk.example.org", @@ -76,49 +80,41 @@ function epochMeWith$( ); } -test("should signal participant not yet connected to livekit", () => { - withTestScheduler(({ behavior, expectObservable }) => { - const { memberships$, membershipsWithTransport$ } = fromMemberships$( - behavior("a", { - a: [bobMembership], - }), - ); +test("should signal participant not yet connected to livekit", async () => { + const mockedMemberships$ = new BehaviorSubject([bobMembership]); + const mockConnectionManagerData$ = new BehaviorSubject( + new ConnectionManagerData(), + ); + const { memberships$, membershipsWithTransport$ } = + createEpochedMemberships$(mockedMemberships$); - const connectionManagerData$ = epochMeWith$( - memberships$, - behavior("a", { - a: new ConnectionManagerData(), - }), - ); + const connectionManagerData$ = epochMeWith$( + memberships$, + mockConnectionManagerData$, + ); - const matrixLivekitMember$ = createMatrixLivekitMembers$({ - scope: testScope, - membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), - connectionManager: { - connectionManagerData$: connectionManagerData$, - } as unknown as IConnectionManager, - }); - - expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", { - a: expect.toSatisfy((data: MatrixLivekitMember[]) => { - expect(data.length).toEqual(1); - expectObservable(data[0].membership$).toBe("a", { - a: bobMembership, - }); - expectObservable(data[0].participant$).toBe("a", { - a: null, - }); - expectObservable(data[0].connection$).toBe("a", { - a: null, - }); - return true; - }), - }); + const matrixLivekitMember$ = createMatrixLivekitMembers$({ + scope: testScope, + membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), + connectionManager: { + connectionManagerData$: connectionManagerData$, + } as unknown as IConnectionManager, }); + + await flushPromises(); + expect(matrixLivekitMember$.value.value).toSatisfy( + (data: MatrixLivekitMember[]) => { + expect(data.length).toEqual(1); + expect(data[0].membership$.value).toBe(bobMembership); + expect(data[0].participant$.value).toBe(null); + expect(data[0].connection$.value).toBe(null); + return true; + }, + ); }); // Helper to create epoch'ed memberships$ and membershipsWithTransport$ from memberships observable. -function fromMemberships$(m$: Observable): { +function createEpochedMemberships$(m$: Observable): { memberships$: Observable>; membershipsWithTransport$: Observable< Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> @@ -143,32 +139,115 @@ function fromMemberships$(m$: Observable): { }; } -test("should signal participant on a connection that is publishing", () => { - withTestScheduler(({ behavior, expectObservable }) => { - const bobParticipantId = getParticipantId( +test("should signal participant on a connection that is publishing", async () => { + const bobParticipantId = fallbackMemberId( + bobMembership.userId, + bobMembership.deviceId, + ); + + const { memberships$, membershipsWithTransport$ } = createEpochedMemberships$( + constant([bobMembership]), + ); + + const connection = { + transport: bobMembership.getTransport(bobMembership), + } as unknown as Connection; + const dataWithPublisher = new ConnectionManagerData(); + dataWithPublisher.add(connection, [ + mockRemoteParticipant({ identity: bobParticipantId }), + ]); + + const connectionManagerData$ = epochMeWith$( + memberships$, + constant(dataWithPublisher), + ); + + const matrixLivekitMember$ = createMatrixLivekitMembers$({ + scope: testScope, + membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), + connectionManager: { + connectionManagerData$: connectionManagerData$, + } as unknown as IConnectionManager, + }); + + await flushPromises(); + expect(matrixLivekitMember$.value.value).toSatisfy( + (data: MatrixLivekitMember[]) => { + expect(data.length).toEqual(1); + expect(data[0].membership$.value).toBe(bobMembership); + expect(data[0].participant$.value).toSatisfy((participant) => { + expect(participant).toBeDefined(); + expect(participant!.identity).toEqual(bobParticipantId); + return true; + }); + expect(data[0].connection$.value).toBe(connection); + return true; + }, + ); +}); + +test("should signal participant on a connection that is not publishing", async () => { + const { memberships$, membershipsWithTransport$ } = createEpochedMemberships$( + constant([bobMembership]), + ); + + const connection = { + transport: bobMembership.getTransport(bobMembership), + } as unknown as Connection; + const dataWithPublisher = new ConnectionManagerData(); + dataWithPublisher.add(connection, []); + + const connectionManagerData$ = epochMeWith$( + memberships$, + constant(dataWithPublisher), + ); + + const matrixLivekitMember$ = createMatrixLivekitMembers$({ + scope: testScope, + membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), + connectionManager: { + connectionManagerData$: connectionManagerData$, + } as unknown as IConnectionManager, + }); + await flushPromises(); + expect(matrixLivekitMember$.value.value).toSatisfy( + (data: MatrixLivekitMember[]) => { + expect(data.length).toEqual(1); + expect(data[0].membership$.value).toBe(bobMembership); + expect(data[0].participant$.value).toBe(null); + expect(data[0].connection$.value).toBe(connection); + return true; + }, + ); +}); + +describe("Publication edge case", () => { + test("bob is publishing in several connections", async () => { + const { memberships$, membershipsWithTransport$ } = + createEpochedMemberships$(constant([bobMembership, carlMembership])); + + const connectionWithPublisher = new ConnectionManagerData(); + const bobParticipantId = fallbackMemberId( bobMembership.userId, bobMembership.deviceId, ); - - const { memberships$, membershipsWithTransport$ } = fromMemberships$( - behavior("a", { - a: [bobMembership], - }), - ); - - const connection = { - transport: bobMembership.getTransport(bobMembership), + const connectionA = { + transport: transportA, } as unknown as Connection; - const dataWithPublisher = new ConnectionManagerData(); - dataWithPublisher.add(connection, [ + const connectionB = { + transport: transportB, + } as unknown as Connection; + + connectionWithPublisher.add(connectionA, [ + mockRemoteParticipant({ identity: bobParticipantId }), + ]); + connectionWithPublisher.add(connectionB, [ mockRemoteParticipant({ identity: bobParticipantId }), ]); const connectionManagerData$ = epochMeWith$( memberships$, - behavior("a", { - a: dataWithPublisher, - }), + constant(connectionWithPublisher), ); const matrixLivekitMember$ = createMatrixLivekitMembers$({ @@ -178,207 +257,73 @@ test("should signal participant on a connection that is publishing", () => { connectionManagerData$: connectionManagerData$, } as unknown as IConnectionManager, }); + await flushPromises(); + expect(matrixLivekitMember$.value.value).toSatisfy( + (data: MatrixLivekitMember[]) => { + expect(data.length).toEqual(2); + expect(data[0].membership$.value).toBe(bobMembership); + expect(data[0].connection$.value).toBe(connectionA); + expect(data[0].participant$.value).toSatisfy((participant) => { + expect(participant).toBeDefined(); + expect(participant!.identity).toEqual(bobParticipantId); + return true; + }); - expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", { - a: expect.toSatisfy((data: MatrixLivekitMember[]) => { - expect(data.length).toEqual(1); - expectObservable(data[0].membership$).toBe("a", { - a: bobMembership, - }); - expectObservable(data[0].participant$).toBe("a", { - a: expect.toSatisfy((participant) => { - expect(participant).toBeDefined(); - expect(participant!.identity).toEqual(bobParticipantId); - return true; - }), - }); - expectObservable(data[0].connection$).toBe("a", { - a: connection, - }); return true; - }), - }); - }); -}); - -test("should signal participant on a connection that is not publishing", () => { - withTestScheduler(({ behavior, expectObservable }) => { - const { memberships$, membershipsWithTransport$ } = fromMemberships$( - behavior("a", { - a: [bobMembership], - }), + }, ); - - const connection = { - transport: bobMembership.getTransport(bobMembership), - } as unknown as Connection; - const dataWithPublisher = new ConnectionManagerData(); - dataWithPublisher.add(connection, []); - - const connectionManagerData$ = epochMeWith$( - memberships$, - behavior("a", { - a: dataWithPublisher, - }), - ); - - const matrixLivekitMember$ = createMatrixLivekitMembers$({ - scope: testScope, - membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), - connectionManager: { - connectionManagerData$: connectionManagerData$, - } as unknown as IConnectionManager, - }); - - expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", { - a: expect.toSatisfy((data: MatrixLivekitMember[]) => { - expect(data.length).toEqual(1); - expectObservable(data[0].membership$).toBe("a", { - a: bobMembership, - }); - expectObservable(data[0].participant$).toBe("a", { - a: null, - }); - expectObservable(data[0].connection$).toBe("a", { - a: connection, - }); - return true; - }), - }); }); }); -describe("Publication edge case", () => { - test("bob is publishing in several connections", () => { - withTestScheduler(({ behavior, expectObservable }) => { - const { memberships$, membershipsWithTransport$ } = fromMemberships$( - behavior("a", { - a: [bobMembership, carlMembership], - }), - ); +test("bob is publishing in the wrong connection", async () => { + const mockedMemberships$ = new BehaviorSubject([ + bobMembership, + carlMembership, + ]); - const connectionWithPublisher = new ConnectionManagerData(); - const bobParticipantId = getParticipantId( - bobMembership.userId, - bobMembership.deviceId, - ); - const connectionA = { - transport: transportA, - } as unknown as Connection; - const connectionB = { - transport: transportB, - } as unknown as Connection; + const { memberships$, membershipsWithTransport$ } = + createEpochedMemberships$(mockedMemberships$); - connectionWithPublisher.add(connectionA, [ - mockRemoteParticipant({ identity: bobParticipantId }), - ]); - connectionWithPublisher.add(connectionB, [ - mockRemoteParticipant({ identity: bobParticipantId }), - ]); + const connectionWithPublisher = new ConnectionManagerData(); - const connectionManagerData$ = epochMeWith$( - memberships$, - behavior("a", { - a: connectionWithPublisher, - }), - ); + const bobParticipantId = fallbackMemberId( + bobMembership.userId, + bobMembership.deviceId, + ); + const connectionA = { transport: transportA } as unknown as Connection; + const connectionB = { transport: transportB } as unknown as Connection; - const matrixLivekitMember$ = createMatrixLivekitMembers$({ - scope: testScope, - membershipsWithTransport$: testScope.behavior( - membershipsWithTransport$, - ), - connectionManager: { - connectionManagerData$: connectionManagerData$, - } as unknown as IConnectionManager, - }); + // Bob is not publishing on A + connectionWithPublisher.add(connectionA, []); + // Bob is publishing on B but his membership says A + connectionWithPublisher.add(connectionB, [ + mockRemoteParticipant({ identity: bobParticipantId }), + ]); - expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe( - "a", - { - a: expect.toSatisfy((data: MatrixLivekitMember[]) => { - expect(data.length).toEqual(2); - expectObservable(data[0].membership$).toBe("a", { - a: bobMembership, - }); - expectObservable(data[0].connection$).toBe("a", { - // The real connection should be from transportA as per the membership - a: connectionA, - }); - expectObservable(data[0].participant$).toBe("a", { - a: expect.toSatisfy((participant) => { - expect(participant).toBeDefined(); - expect(participant!.identity).toEqual(bobParticipantId); - return true; - }), - }); - return true; - }), - }, - ); - }); + const connectionsWithPublisher$ = new BehaviorSubject( + connectionWithPublisher, + ); + const connectionManagerData$ = epochMeWith$( + memberships$, + connectionsWithPublisher$, + ); + + const matrixLivekitMember$ = createMatrixLivekitMembers$({ + scope: testScope, + membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), + connectionManager: { + connectionManagerData$: connectionManagerData$, + } as unknown as IConnectionManager, }); - test("bob is publishing in the wrong connection", () => { - withTestScheduler(({ behavior, expectObservable }) => { - const { memberships$, membershipsWithTransport$ } = fromMemberships$( - behavior("a", { - a: [bobMembership, carlMembership], - }), - ); - - const connectionWithPublisher = new ConnectionManagerData(); - const bobParticipantId = getParticipantId( - bobMembership.userId, - bobMembership.deviceId, - ); - const connectionA = { transport: transportA } as unknown as Connection; - const connectionB = { transport: transportB } as unknown as Connection; - - // Bob is not publishing on A - connectionWithPublisher.add(connectionA, []); - // Bob is publishing on B but his membership says A - connectionWithPublisher.add(connectionB, [ - mockRemoteParticipant({ identity: bobParticipantId }), - ]); - - const connectionManagerData$ = epochMeWith$( - memberships$, - behavior("a", { - a: connectionWithPublisher, - }), - ); - - const matrixLivekitMember$ = createMatrixLivekitMembers$({ - scope: testScope, - membershipsWithTransport$: testScope.behavior( - membershipsWithTransport$, - ), - connectionManager: { - connectionManagerData$: connectionManagerData$, - } as unknown as IConnectionManager, - }); - - expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe( - "a", - { - a: expect.toSatisfy((data: MatrixLivekitMember[]) => { - expect(data.length).toEqual(2); - expectObservable(data[0].membership$).toBe("a", { - a: bobMembership, - }); - expectObservable(data[0].connection$).toBe("a", { - // The real connection should be from transportA as per the membership - a: connectionA, - }); - expectObservable(data[0].participant$).toBe("a", { - // No participant as Bob is not publishing on his membership transport - a: null, - }); - return true; - }), - }, - ); - }); - }); + await flushPromises(); + expect(matrixLivekitMember$.value.value).toSatisfy( + (data: MatrixLivekitMember[]) => { + expect(data.length).toEqual(2); + expect(data[0].membership$.value).toBe(bobMembership); + expect(data[0].connection$.value).toBe(connectionA); + expect(data[0].participant$.value).toBe(null); + return true; + }, + ); }); diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index 79ad933c..9c6a05c9 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -13,8 +13,11 @@ import { type LivekitTransport, type CallMembership, } from "matrix-js-sdk/lib/matrixrtc"; -import { combineLatest, filter, map } from "rxjs"; +import { combineLatest, filter, map, switchMap } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +import { sha256 } from "matrix-js-sdk/lib/digest"; +import { encodeUnpaddedBase64Url } from "matrix-js-sdk"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type Behavior } from "../../Behavior"; import { type IConnectionManager } from "./ConnectionManager"; @@ -62,64 +65,89 @@ export function createMatrixLivekitMembers$({ membershipsWithTransport$, connectionManager, }: Props): Behavior> { + /** + * This internal observable is used to compute the async sha256 hash of the user's identity. + * a promise is treated like an observable. So we can switchMap on the promise from the identity computation. + * The last update to `membershipsWithTransport$` will always be the last promise we pass to switchMap. + * So we will eventually always end up with the latest memberships and their identities. + */ + const membershipsWithTransportAndLivekitIdentity$ = + membershipsWithTransport$.pipe( + switchMap(async (membershipsWithTransport) => { + const { value, epoch } = membershipsWithTransport; + const membershipsWithTransportAndLkIdentityPromises = value.map( + async (obj) => { + return computeLivekitParticipantIdentity( + obj.membership, + obj.membership.kind, + ); + }, + ); + const identities = await Promise.all( + membershipsWithTransportAndLkIdentityPromises, + ); + const membershipsWithTransportAndLkIdentity = value.map( + ({ transport, membership }, index) => { + return { transport, membership, identity: identities[index] }; + }, + ); + return new Epoch(membershipsWithTransportAndLkIdentity, epoch); + }), + ); + /** * Stream of all the call members and their associated livekit data (if available). */ - return scope.behavior( combineLatest([ - membershipsWithTransport$, + membershipsWithTransportAndLivekitIdentity$, connectionManager.connectionManagerData$, ]).pipe( filter((values) => values.every((value) => value.epoch === values[0].epoch), ), - map( - ([ - { value: membershipsWithTransports, epoch }, - { value: managerData }, - ]) => - new Epoch([membershipsWithTransports, managerData] as const, epoch), - ), + map(([x, y]) => new Epoch([x.value, y.value] as const, x.epoch)), generateItemsWithEpoch( // Generator function. // creates an array of `{key, data}[]` // Each change in the keys (new key, missing key) will result in a call to the factory function. - function* ([membershipsWithTransports, managerData]) { - for (const { membership, transport } of membershipsWithTransports) { - // TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to - const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`; - + function* ([membershipsWithTransportAndLivekitIdentity, managerData]) { + for (const { + membership, + transport, + identity, + } of membershipsWithTransportAndLivekitIdentity) { const participants = transport ? managerData.getParticipantForTransport(transport) : []; const participant = - participants.find((p) => p.identity == participantId) ?? null; + participants.find((p) => p.identity == identity) ?? null; const connection = transport ? managerData.getConnectionForTransport(transport) : null; yield { - keys: [participantId, membership.userId], + keys: [identity, membership.userId, membership.deviceId], data: { membership, participant, connection }, }; } }, // Each update where the key of the generator array do not change will result in updates to the `data$` observable in the factory. - (scope, data$, participantId, userId) => { + (scope, data$, identity, userId, deviceId) => { logger.debug( - `Generating member for participantId: ${participantId}, userId: ${userId}`, + `Generating member for livekitIdentity: ${identity}, userId:deviceId: ${userId}${deviceId}`, ); // will only get called once per `participantId, userId` pair. // updates to data$ and as a result to displayName$ and mxcAvatarUrl$ are more frequent. return { - participantId, + identity, userId, ...scope.splitBehavior(data$), }; }, ), ), + new Epoch([], -1), ); } @@ -136,3 +164,42 @@ export function areLivekitTransportsEqual( if (!t1 && !t2) return true; return false; } + +const livekitParticipantIdentityCache = new Map(); + +/** + * The string that is computed based on the membership and used for the computing the hash. + * `${userId}:${deviceId}:${membershipID}` + * as the direct imput for: await sha256(input) + */ +export const livekitIdentityInput = ({ + userId, + deviceId, + memberId, +}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`; + +export async function computeLivekitParticipantIdentity( + membership: CallMembershipIdentityParts, + kind: "rtc" | "session", +): Promise { + switch (kind) { + case "rtc": { + const input = livekitIdentityInput(membership); + if (livekitParticipantIdentityCache.size > 400) + // prevent memory leaks in a stupid/simple way + livekitParticipantIdentityCache.clear(); + // TODO use non deprecated memberId + if (livekitParticipantIdentityCache.has(input)) + return livekitParticipantIdentityCache.get(input)!; + else { + const hashBuffer = await sha256(input); + const hashedString = encodeUnpaddedBase64Url(hashBuffer); + livekitParticipantIdentityCache.set(input, hashedString); + return hashedString; + } + } + case "session": + default: + return `${membership.userId}:${membership.deviceId}`; + } +} diff --git a/yarn.lock b/yarn.lock index 94b73130..707a6480 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2795,10 +2795,10 @@ __metadata: languageName: node linkType: hard -"@matrix-org/matrix-sdk-crypto-wasm@npm:^15.3.0": - version: 15.3.0 - resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:15.3.0" - checksum: 10c0/45628f36b7b0e54a8777ae67a7233dbdf3e3cf14e0d95d21f62f89a7ea7e3f907232f1eb7b1262193b1e227759fad47af829dcccc103ded89011f13c66f01d76 +"@matrix-org/matrix-sdk-crypto-wasm@npm:^16.0.0": + version: 16.0.0 + resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:16.0.0" + checksum: 10c0/13b4ede3e618da819957abff778afefcf3baf9a2faac04a36bb5a07a44fae2ea05fbfa072eb3408d48b2b7b9aaf27242ce52c594c8ce9bf1fb8b3aade2832be1 languageName: node linkType: hard @@ -6571,24 +6571,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001688": - version: 1.0.30001701 - resolution: "caniuse-lite@npm:1.0.30001701" - checksum: 10c0/a814bd4dd8b49645ca51bc6ee42120660a36394bb54eb6084801d3f2bbb9471e5e1a9a8a25f44f83086a032d46e66b33031e2aa345f699b90a7e84a9836b819c - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.30001702": - version: 1.0.30001720 - resolution: "caniuse-lite@npm:1.0.30001720" - checksum: 10c0/ba9f963364ec4bfc8359d15d7e2cf365185fa1fddc90b4f534c71befedae9b3dd0cd2583a25ffc168a02d7b61b6c18b59bda0a1828ea2a5250fd3e35c2c049e9 - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.30001726": - version: 1.0.30001726 - resolution: "caniuse-lite@npm:1.0.30001726" - checksum: 10c0/2c5f91da7fd9ebf8c6b432818b1498ea28aca8de22b30dafabe2a2a6da1e014f10e67e14f8e68e872a0867b6b4cd6001558dde04e3ab9770c9252ca5c8849d0e +"caniuse-lite@npm:^1.0.30001688, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001726": + version: 1.0.30001760 + resolution: "caniuse-lite@npm:1.0.30001760" + checksum: 10c0/cee26dff5c5b15ba073ab230200e43c0d4e88dc3bac0afe0c9ab963df70aaa876c3e513dde42a027f317136bf6e274818d77b073708b74c5807dfad33c029d3c languageName: node linkType: hard @@ -7547,7 +7533,7 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "npm:^39.2.0" + matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00" matrix-widget-api: "npm:^1.14.0" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -10352,12 +10338,12 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@npm:^39.2.0": - version: 39.2.0 - resolution: "matrix-js-sdk@npm:39.2.0" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00": + version: 39.3.0 + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00" dependencies: "@babel/runtime": "npm:^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.3.0" + "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" another-json: "npm:^0.2.0" bs58: "npm:^6.0.0" content-type: "npm:^1.0.4" @@ -10370,7 +10356,7 @@ __metadata: sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/f8b5261de2744305330ba3952821ca9303698170bfd3a0ff8a767b9286d4e8d4ed5aaf6fbaf8a1e8ff9dbd859102a2a47d882787e2da3b3078965bec00157959 + checksum: 10c0/9607b0c063c873a24c1a2d05cc7500d60c32556ec82b666ebaae5c5e829faf5bb7639780efddea7211e6b9873098bd53b97656f041e932e8b0de0c208ccabbff languageName: node linkType: hard From 5bc6ed5885c8de4220f7542fe4382c727c03629f Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 15 Dec 2025 20:17:57 +0100 Subject: [PATCH 05/76] small refactor to make it testable. --- src/room/CallEventAudioRenderer.test.tsx | 8 +++ src/room/InCallView.test.tsx | 10 ++- src/state/CallViewModel/CallViewModel.test.ts | 22 +++++-- .../LivekitParticipantIdentity.ts | 53 +++++++++++++++ .../MatrixLivekitMembers.test.ts | 1 - .../remoteMembers/MatrixLivekitMembers.ts | 66 ++++--------------- .../remoteMembers/integration.test.ts | 6 ++ src/utils/test.ts | 28 +++++++- 8 files changed, 134 insertions(+), 60 deletions(-) create mode 100644 src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 733346eb..38f56b14 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -23,6 +23,7 @@ import { import { exampleTransport, + mockComputeLivekitParticipantIdentity$, mockMatrixRoomMember, mockRtcMembership, } from "../utils/test"; @@ -47,6 +48,13 @@ vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({ ...(await importOriginal()), makeTransport: (): [LivekitTransport] => [exampleTransport], })); +vitest.mock( + import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"), + async (importOriginal) => ({ + ...(await importOriginal()), + computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, + }), +); afterEach(() => { vitest.clearAllMocks(); diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index a137074b..cd0af547 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -22,8 +22,8 @@ import { BrowserRouter } from "react-router-dom"; import { TooltipProvider } from "@vector-im/compound-web"; import { RoomContext, useLocalParticipant } from "@livekit/components-react"; -import { InCallView } from "./InCallView"; import { + mockComputeLivekitParticipantIdentity$, mockLivekitRoom, mockLocalParticipant, mockMatrixRoom, @@ -34,6 +34,7 @@ import { mockRtcMembership, type MockRTCSession, } from "../utils/test"; +import { InCallView } from "./InCallView"; import { E2eeType } from "../e2ee/e2eeType"; import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; import { alice, local } from "../utils/test-fixtures"; @@ -61,6 +62,13 @@ vi.mock("../livekit/MatrixAudioRenderer"); vi.mock("react-use-measure", () => ({ default: (): [() => void, object] => [(): void => {}, {}], })); +vi.mock( + import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"), + async (importOriginal) => ({ + ...(await importOriginal()), + computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, + }), +); const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); const localParticipant = mockLocalParticipant({ diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index be598702..58814bbe 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -44,6 +44,7 @@ import { mockRtcMembership, testScope, exampleTransport, + mockComputeLivekitParticipantIdentity$, } from "../../utils/test.ts"; import { E2eeType } from "../../e2ee/e2eeType.ts"; import { @@ -77,11 +78,22 @@ vi.mock("../e2ee/matrixKeyProvider"); const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); vi.mock("../UrlParams", () => ({ getUrlParams })); -vi.mock("../rtcSessionHelpers", async (importOriginal) => ({ - ...(await importOriginal()), - makeTransport: async (): Promise => - Promise.resolve(exampleTransport), -})); +vi.mock( + "../state/CallViewModel/localMember/localTransport", + async (importOriginal) => ({ + ...(await importOriginal()), + makeTransport: async (): Promise => + Promise.resolve(exampleTransport), + }), +); + +vi.mock( + import("./remoteMembers/LivekitParticipantIdentity.ts"), + async (importOriginal) => ({ + ...(await importOriginal()), + computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, + }), +); const yesNo = { y: true, diff --git a/src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts b/src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts new file mode 100644 index 00000000..e43d0bd1 --- /dev/null +++ b/src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts @@ -0,0 +1,53 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { encodeUnpaddedBase64Url } from "matrix-js-sdk"; +import { sha256 } from "matrix-js-sdk/lib/digest"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; +import { from, type Observable } from "rxjs"; + +const livekitParticipantIdentityCache = new Map(); + +/** + * The string that is computed based on the membership and used for the computing the hash. + * `${userId}:${deviceId}:${membershipID}` + * as the direct imput for: await sha256(input) + */ +export const livekitIdentityInput = ({ + userId, + deviceId, + memberId, +}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`; + +export function computeLivekitParticipantIdentity$( + membership: CallMembershipIdentityParts, + kind: "rtc" | "session", +): Observable { + const compute = async (): Promise => { + switch (kind) { + case "rtc": { + const input = livekitIdentityInput(membership); + if (livekitParticipantIdentityCache.size > 400) + // prevent memory leaks in a stupid/simple way + livekitParticipantIdentityCache.clear(); + // TODO use non deprecated memberId + if (livekitParticipantIdentityCache.has(input)) + return livekitParticipantIdentityCache.get(input)!; + else { + const hashBuffer = await sha256(input); + const hashedString = encodeUnpaddedBase64Url(hashBuffer); + livekitParticipantIdentityCache.set(input, hashedString); + return hashedString; + } + } + case "session": + default: + return `${membership.userId}:${membership.deviceId}`; + } + }; + return from(compute()); +} diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts index c2e60798..68a67546 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts @@ -28,7 +28,6 @@ import { flushPromises, mockCallMembership, mockRemoteParticipant, - withTestScheduler, } from "../../../utils/test.ts"; import { type Connection } from "./Connection.ts"; import { constant } from "../../Behavior.ts"; diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index 9c6a05c9..30abfc9b 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -15,15 +15,13 @@ import { } from "matrix-js-sdk/lib/matrixrtc"; import { combineLatest, filter, map, switchMap } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; -import { sha256 } from "matrix-js-sdk/lib/digest"; -import { encodeUnpaddedBase64Url } from "matrix-js-sdk"; -import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type Behavior } from "../../Behavior"; import { type IConnectionManager } from "./ConnectionManager"; import { Epoch, type ObservableScope } from "../../ObservableScope"; import { type Connection } from "./Connection"; import { generateItemsWithEpoch } from "../../../utils/observable"; +import { computeLivekitParticipantIdentity$ } from "./LivekitParticipantIdentity"; const logger = rootLogger.getChild("[MatrixLivekitMembers]"); @@ -73,25 +71,28 @@ export function createMatrixLivekitMembers$({ */ const membershipsWithTransportAndLivekitIdentity$ = membershipsWithTransport$.pipe( - switchMap(async (membershipsWithTransport) => { + switchMap((membershipsWithTransport) => { const { value, epoch } = membershipsWithTransport; const membershipsWithTransportAndLkIdentityPromises = value.map( - async (obj) => { - return computeLivekitParticipantIdentity( + (obj) => { + return computeLivekitParticipantIdentity$( obj.membership, obj.membership.kind, ); }, ); - const identities = await Promise.all( + return combineLatest( membershipsWithTransportAndLkIdentityPromises, + ).pipe( + map((identities) => { + const membershipsWithTransportAndLkIdentity = value.map( + ({ transport, membership }, index) => { + return { transport, membership, identity: identities[index] }; + }, + ); + return new Epoch(membershipsWithTransportAndLkIdentity, epoch); + }), ); - const membershipsWithTransportAndLkIdentity = value.map( - ({ transport, membership }, index) => { - return { transport, membership, identity: identities[index] }; - }, - ); - return new Epoch(membershipsWithTransportAndLkIdentity, epoch); }), ); @@ -164,42 +165,3 @@ export function areLivekitTransportsEqual( if (!t1 && !t2) return true; return false; } - -const livekitParticipantIdentityCache = new Map(); - -/** - * The string that is computed based on the membership and used for the computing the hash. - * `${userId}:${deviceId}:${membershipID}` - * as the direct imput for: await sha256(input) - */ -export const livekitIdentityInput = ({ - userId, - deviceId, - memberId, -}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`; - -export async function computeLivekitParticipantIdentity( - membership: CallMembershipIdentityParts, - kind: "rtc" | "session", -): Promise { - switch (kind) { - case "rtc": { - const input = livekitIdentityInput(membership); - if (livekitParticipantIdentityCache.size > 400) - // prevent memory leaks in a stupid/simple way - livekitParticipantIdentityCache.clear(); - // TODO use non deprecated memberId - if (livekitParticipantIdentityCache.has(input)) - return livekitParticipantIdentityCache.get(input)!; - else { - const hashBuffer = await sha256(input); - const hashedString = encodeUnpaddedBase64Url(hashBuffer); - livekitParticipantIdentityCache.set(input, hashedString); - return hashedString; - } - } - case "session": - default: - return `${membership.userId}:${membership.deviceId}`; - } -} diff --git a/src/state/CallViewModel/remoteMembers/integration.test.ts b/src/state/CallViewModel/remoteMembers/integration.test.ts index e3aa6be8..3ad81fa8 100644 --- a/src/state/CallViewModel/remoteMembers/integration.test.ts +++ b/src/state/CallViewModel/remoteMembers/integration.test.ts @@ -22,6 +22,7 @@ import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import { mockCallMembership, + mockComputeLivekitParticipantIdentity$, mockMediaDevices, withTestScheduler, } from "../../../utils/test.ts"; @@ -43,6 +44,11 @@ let lkRoomFactory: () => LivekitRoom; const createdMockLivekitRooms: Map = new Map(); +vi.mock(import("./LivekitParticipantIdentity.ts"), async (importOriginal) => ({ + ...(await importOriginal()), + computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, +})); + beforeEach(() => { testScope = new ObservableScope(); mockClient = { diff --git a/src/utils/test.ts b/src/utils/test.ts index bd7dcd6f..28fc8546 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -15,6 +15,7 @@ import { vitest, } from "vitest"; import { + encodeUnpaddedBase64, MatrixEvent, type Room as MatrixRoom, type Room, @@ -43,13 +44,14 @@ import { type Room as LivekitRoom, Track, } from "livekit-client"; -import { randomUUID } from "crypto"; +import { createHash, randomUUID } from "crypto"; import { type TrackReference } from "@livekit/components-core"; import EventEmitter from "events"; import { type KeyTransportEvents, type KeyTransportEventsHandlerMap, } from "matrix-js-sdk/lib/matrixrtc/IKeyTransport"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { LocalUserMediaViewModel, @@ -522,3 +524,27 @@ export function mockMuteStates( const observableScope = new ObservableScope(); return new MuteStates(observableScope, mockMediaDevices({}), joined$); } + +export const mockComputeLivekitParticipantIdentity$ = ( + membership: CallMembershipIdentityParts, + kind: "rtc" | "session", +): Observable => { + function sha256(commitmentStr: string): string { + return encodeUnpaddedBase64( + createHash("sha256").update(commitmentStr, "utf8").digest(), + ); + } + let hash; + switch (kind) { + case "rtc": { + hash = sha256( + `${membership.userId}|${membership.deviceId}|${membership.memberId}`, + ); + break; + } + case "session": + default: + hash = `${membership.userId}:${membership.deviceId}`; + } + return of(hash); +}; From 6fe6daba313b2cf55f582baf49d1e8f5091b5ed2 Mon Sep 17 00:00:00 2001 From: fkwp Date: Tue, 16 Dec 2025 11:20:38 +0100 Subject: [PATCH 06/76] switch synapse docker image to one with sticky event support --- dev-backend-docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index c7591847..5e955831 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -88,7 +88,7 @@ services: synapse: hostname: homeserver - image: docker.io/matrixdotorg/synapse:latest + image: ghcr.io/element-hq/synapse:pr-18968-dcb7678281bc02d4551043a6338fe5b7e6aa47ce pull_policy: always environment: - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml @@ -106,7 +106,7 @@ services: synapse-1: hostname: homeserver-1 - image: docker.io/matrixdotorg/synapse:latest + image: ghcr.io/element-hq/synapse:pr-18968-dcb7678281bc02d4551043a6338fe5b7e6aa47ce pull_policy: always environment: - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml From ff3d6bd088f5024207c2921830b374e0e9c0fa71 Mon Sep 17 00:00:00 2001 From: fkwp Date: Tue, 16 Dec 2025 11:22:25 +0100 Subject: [PATCH 07/76] enable sticky events --- backend/dev_homeserver-othersite.yaml | 2 ++ backend/dev_homeserver.yaml | 2 +- backend/playwright_homeserver-othersite.yaml | 2 ++ backend/playwright_homeserver.yaml | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/dev_homeserver-othersite.yaml b/backend/dev_homeserver-othersite.yaml index 947e33cd..81e775ca 100644 --- a/backend/dev_homeserver-othersite.yaml +++ b/backend/dev_homeserver-othersite.yaml @@ -38,6 +38,8 @@ experimental_features: # MSC4222 needed for syncv2 state_after. This allow clients to # correctly track the state of the room. msc4222_enabled: true + # sticky events for MatrixRTC user state + msc4354_enabled: true # The maximum allowed duration by which sent events can be delayed, as # per MSC4140. Must be a positive value if set. Defaults to no diff --git a/backend/dev_homeserver.yaml b/backend/dev_homeserver.yaml index fe89d95a..dc7b42c8 100644 --- a/backend/dev_homeserver.yaml +++ b/backend/dev_homeserver.yaml @@ -38,7 +38,7 @@ experimental_features: # MSC4222 needed for syncv2 state_after. This allow clients to # correctly track the state of the room. msc4222_enabled: true - # sticky events for matrixRTC user state + # sticky events for MatrixRTC user state msc4354_enabled: true # The maximum allowed duration by which sent events can be delayed, as diff --git a/backend/playwright_homeserver-othersite.yaml b/backend/playwright_homeserver-othersite.yaml index 5cb0dd65..35640ae9 100644 --- a/backend/playwright_homeserver-othersite.yaml +++ b/backend/playwright_homeserver-othersite.yaml @@ -38,6 +38,8 @@ experimental_features: # MSC4222 needed for syncv2 state_after. This allow clients to # correctly track the state of the room. msc4222_enabled: true + # sticky events for MatrixRTC user state + msc4354_enabled: true # The maximum allowed duration by which sent events can be delayed, as # per MSC4140. Must be a positive value if set. Defaults to no diff --git a/backend/playwright_homeserver.yaml b/backend/playwright_homeserver.yaml index 0d7b175c..a83247cd 100644 --- a/backend/playwright_homeserver.yaml +++ b/backend/playwright_homeserver.yaml @@ -38,6 +38,8 @@ experimental_features: # MSC4222 needed for syncv2 state_after. This allow clients to # correctly track the state of the room. msc4222_enabled: true + # sticky events for MatrixRTC user state + msc4354_enabled: true # The maximum allowed duration by which sent events can be delayed, as # per MSC4140. Must be a positive value if set. Defaults to no From ab7e3486b3ea1953f1435acbced309189476e78b Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 17 Dec 2025 09:53:49 +0100 Subject: [PATCH 08/76] Make use of the new jwt service endpoint (with delayed event delegation) This also does all the compatibility work. When to use which endpoint to authenticate agains a jwt service. --- src/livekit/openIDSFU.ts | 90 ++++++++++++++++--- src/state/CallViewModel/CallViewModel.ts | 32 ++++++- .../localMember/LocalMember.test.ts | 3 + .../CallViewModel/localMember/LocalMember.ts | 5 +- .../localMember/LocalTransport.test.ts | 18 +++- .../localMember/LocalTransport.ts | 63 +++++++++---- .../remoteMembers/Connection.test.ts | 17 ++-- .../CallViewModel/remoteMembers/Connection.ts | 20 ++++- .../remoteMembers/ConnectionFactory.ts | 18 +++- .../remoteMembers/ConnectionManager.test.ts | 13 ++- .../remoteMembers/ConnectionManager.ts | 27 ++++-- .../remoteMembers/ECConnectionFactory.test.ts | 20 ++++- .../remoteMembers/MatrixLivekitMembers.ts | 6 +- .../remoteMembers/integration.test.ts | 2 + src/state/SessionBehaviors.ts | 15 +++- src/utils/test.ts | 10 ++- yarn.lock | 9 +- 17 files changed, 294 insertions(+), 74 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 3ae003fb..f07bb035 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -7,9 +7,11 @@ Please see LICENSE in the repository root for full details. import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { FailToGetOpenIdToken } from "../utils/errors"; import { doNetworkOperationWithRetry } from "../utils/matrix"; +import { Config } from "../config/Config"; export interface SFUConfig { url: string; @@ -33,8 +35,12 @@ export type OpenIDClientParts = Pick< */ export async function getSFUConfigWithOpenID( client: OpenIDClientParts, + membership: CallMembershipIdentityParts, serviceUrl: string, - matrixRoomId: string, + livekitRoomAlias: string, + matrix2jwt: boolean, + delayEndpointBaseUrl?: string, + delayId?: string, ): Promise { let openIdToken: IOpenIDToken; try { @@ -49,21 +55,31 @@ export async function getSFUConfigWithOpenID( logger.debug("Got openID token", openIdToken); logger.info(`Trying to get JWT for focus ${serviceUrl}...`); - const sfuConfig = await getLiveKitJWT( - client, + const args: [CallMembershipIdentityParts, string, string, IOpenIDToken] = [ + membership, serviceUrl, - matrixRoomId, + livekitRoomAlias, openIdToken, - ); - logger.info(`Got JWT from call's active focus URL.`); - - return sfuConfig; + ]; + if (matrix2jwt) { + const sfuConfig = await getLiveKitJWTWithDelayDelegation( + ...args, + delayEndpointBaseUrl, + delayId, + ); + logger.info(`Got JWT from call's active focus URL.`); + return sfuConfig; + } else { + const sfuConfig = await getLiveKitJWT(...args); + logger.info(`Got JWT from call's active focus URL.`); + return sfuConfig; + } } async function getLiveKitJWT( - client: OpenIDClientParts, + membership: CallMembershipIdentityParts, livekitServiceURL: string, - roomName: string, + livekitRoomAlias: string, openIDToken: IOpenIDToken, ): Promise { try { @@ -73,9 +89,9 @@ async function getLiveKitJWT( "Content-Type": "application/json", }, body: JSON.stringify({ - room: roomName, + room: livekitRoomAlias, openid_token: openIDToken, - device_id: client.getDeviceId(), + device_id: membership.deviceId, }), }); if (!res.ok) { @@ -86,3 +102,53 @@ async function getLiveKitJWT( throw new Error("SFU Config fetch failed with exception " + e); } } + +export async function getLiveKitJWTWithDelayDelegation( + membership: CallMembershipIdentityParts, + livekitServiceURL: string, + livekitRoomAlias: string, + openIDToken: IOpenIDToken, + delayEndpointBaseUrl?: string, + delayId?: string, +): Promise { + const { userId, deviceId, memberId } = membership; + + const body = { + room_id: livekitRoomAlias, + slot_id: "m.call#ROOM", + openid_token: openIDToken, + member: { + id: memberId, + claimed_user_id: userId, + claimed_device_id: deviceId, + }, + }; + + let bodyDalayParts = {}; + // Also check for empty string + if (delayId && delayEndpointBaseUrl) { + const delayTimeoutMs = + Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms ?? 1000; + bodyDalayParts = { + delay_id: delayId, + delay_timeout: delayTimeoutMs, + delay_cs_api_url: delayEndpointBaseUrl, + }; + } + + try { + const res = await fetch(livekitServiceURL + "/get_token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...body, ...bodyDalayParts }), + }); + if (!res.ok) { + throw new Error("SFU Config fetch failed with status code " + res.status); + } + return await res.json(); + } catch (e) { + throw new Error("SFU Config fetch failed with exception " + e); + } +} diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index e1869cf6..23c58268 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -41,10 +41,12 @@ import { } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { + MembershipManagerEvent, type LivekitTransport, type MatrixRTCSession, } from "matrix-js-sdk/lib/matrixrtc"; import { type IWidgetApiRequest } from "matrix-widget-api"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { LocalUserMediaViewModel, @@ -98,7 +100,7 @@ import { type SpotlightLandscapeLayoutMedia, type SpotlightPortraitLayoutMedia, } from "../layout-types.ts"; -import { ElementCallError } from "../../utils/errors.ts"; +import { ElementCallError, UnknownCallError } from "../../utils/errors.ts"; import { type ObservableScope } from "../ObservableScope.ts"; import { createHomeserverConnected$ } from "./localMember/HomeserverConnected.ts"; import { @@ -375,8 +377,11 @@ export function createCallViewModel$( trackProcessorState$: Behavior, ): CallViewModel { const client = matrixRoom.client; - const userId = client.getUserId()!; - const deviceId = client.getDeviceId()!; + const userId = client.getUserId(); + const deviceId = client.getDeviceId(); + if (!(userId && deviceId)) + throw new UnknownCallError(new Error("userId and deviceId are required")); + const livekitKeyProvider = getE2eeKeyProvider( options.encryptionSystem, matrixRTCSession, @@ -407,10 +412,29 @@ export function createCallViewModel$( memberships$, ); + const ownMembershipIdentity: CallMembershipIdentityParts = { + userId, + deviceId, + memberId: `${userId}:${deviceId}`, + }; + const localTransport$ = createLocalTransport$({ scope: scope, memberships$: memberships$, + ownMembershipIdentity, client, + useMatrix2$: scope.behavior( + options.matrixRTCMode$.pipe(map((v) => v === MatrixRTCMode.Matrix_2_0)), + ), + delayId$: scope.behavior( + ( + fromEvent( + matrixRTCSession, + MembershipManagerEvent.DelayIdChanged, + ) as Observable + ).pipe(map((v) => v ?? null)), + matrixRTCSession.delayId ?? null, + ), roomId: matrixRoom.roomId, useOldestMember$: scope.behavior( options.matrixRTCMode$.pipe(map((v) => v === MatrixRTCMode.Legacy)), @@ -455,6 +479,7 @@ export function createCallViewModel$( ), ), logger: logger, + ownMembershipIdentity, }); const matrixLivekitMembers$ = createMatrixLivekitMembers$({ @@ -485,6 +510,7 @@ export function createCallViewModel$( joinMatrixRTC: (transport: LivekitTransport) => { return enterRTCSession( matrixRTCSession, + ownMembershipIdentity, transport, connectOptions$.value, ); diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index 6a9f196e..8de14039 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -24,6 +24,7 @@ import { mockLivekitRoom, mockMuteStates, withTestScheduler, + ownMemberMock, } from "../../../utils/test"; import { TransportState, @@ -108,6 +109,7 @@ describe("LocalMembership", () => { enterRTCSession( mockedSession, + ownMemberMock, { livekit_alias: "roomId", livekit_service_url: "http://my-well-known-service-url.com", @@ -166,6 +168,7 @@ describe("LocalMembership", () => { enterRTCSession( mockedSession, + ownMemberMock, { livekit_alias: "roomId", livekit_service_url: "http://my-well-known-service-url.com", diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 40fb62d6..6f554423 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -34,6 +34,7 @@ import { } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { deepCompare } from "matrix-js-sdk/lib/utils"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { constant, type Behavior } from "../../Behavior.ts"; import { type IConnectionManager } from "../remoteMembers/ConnectionManager.ts"; @@ -657,6 +658,7 @@ interface EnterRTCSessionOptions { // Exported for unit testing export function enterRTCSession( rtcSession: MatrixRTCSession, + ownMembershipIdentity: CallMembershipIdentityParts, transport: LivekitTransport, { encryptMedia, matrixRTCMode }: EnterRTCSessionOptions, ): void { @@ -674,7 +676,8 @@ export function enterRTCSession( const multiSFU = matrixRTCMode !== MatrixRTCMode.Legacy; // Multi-sfu does not need a preferred foci list. just the focus that is actually used. // TODO where/how do we track errors originating from the ongoing rtcSession? - rtcSession.joinRoomSession( + rtcSession.joinRTCSession( + ownMembershipIdentity, multiSFU ? [] : [transport], multiSFU ? transport : undefined, { diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index c1c36fa5..ba030757 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc"; import { BehaviorSubject } from "rxjs"; -import { mockConfig, flushPromises } from "../../../utils/test"; +import { mockConfig, flushPromises, ownMemberMock } from "../../../utils/test"; import { createLocalTransport$ } from "./LocalTransport"; import { constant } from "../../Behavior"; import { Epoch, ObservableScope } from "../../ObservableScope"; @@ -32,10 +32,14 @@ describe("LocalTransport", () => { memberships$: constant(new Epoch([])), client: { getDomain: () => "", + baseUrl: "example.org", // These won't be called in this error path but satisfy the type getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), }, + ownMembershipIdentity: ownMemberMock, + useMatrix2$: constant(false), + delayId$: constant("delay_id_mock"), }); await flushPromises(); @@ -65,11 +69,15 @@ describe("LocalTransport", () => { useOldestMember$: constant(false), memberships$: constant(new Epoch([])), client: { + baseUrl: "https://lk.example.org", // Use empty domain to skip .well-known and use config directly getDomain: () => "", getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), }, + ownMembershipIdentity: ownMemberMock, + useMatrix2$: constant(false), + delayId$: constant("delay_id_mock"), }); localTransport$.subscribe( (o) => observations.push(o), @@ -105,7 +113,11 @@ describe("LocalTransport", () => { getDomain: () => "", getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), + baseUrl: "https://lk.example.org", }, + ownMembershipIdentity: ownMemberMock, + useMatrix2$: constant(false), + delayId$: constant("delay_id_mock"), }); openIdResolver.resolve?.({ url: "https://lk.example.org", jwt: "jwt" }); @@ -140,7 +152,11 @@ describe("LocalTransport", () => { getDomain: () => "", getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), + baseUrl: "https://lk.example.org", }, + ownMembershipIdentity: ownMemberMock, + useMatrix2$: constant(false), + delayId$: constant("delay_id_mock"), }); openIdResolver.resolve?.({ url: "https://lk.example.org", jwt: "jwt" }); diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 1320b8c4..6c3e1cd0 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -23,6 +23,7 @@ import { } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type Behavior } from "../../Behavior.ts"; import { type Epoch, type ObservableScope } from "../../ObservableScope.ts"; @@ -34,6 +35,7 @@ import { } from "../../../livekit/openIDSFU.ts"; import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts"; import { customLivekitUrl } from "../../../settings/settings.ts"; +import { type LivekitTransportWithVersion } from "../remoteMembers/ConnectionManager.ts"; const logger = rootLogger.getChild("[LocalTransport]"); @@ -44,10 +46,13 @@ const logger = rootLogger.getChild("[LocalTransport]"); */ interface Props { scope: ObservableScope; + ownMembershipIdentity: CallMembershipIdentityParts; memberships$: Behavior>; - client: Pick & OpenIDClientParts; + client: Pick & OpenIDClientParts; roomId: string; useOldestMember$: Behavior; + useMatrix2$: Behavior; + delayId$: Behavior; } /** @@ -62,20 +67,26 @@ interface Props { export const createLocalTransport$ = ({ scope, memberships$, + ownMembershipIdentity, client, roomId, useOldestMember$, -}: Props): Behavior => { + useMatrix2$, + delayId$, +}: Props): Behavior => { /** * The transport over which we should be actively publishing our media. * undefined when not joined. */ const oldestMemberTransport$ = scope.behavior( memberships$.pipe( - map( - (memberships) => - memberships.value[0]?.getTransport(memberships.value[0]) ?? null, - ), + map((memberships) => { + const oldestMember = memberships.value[0]; + const t = oldestMember?.getTransport(memberships.value[0]); + if (!t) return null; + // Here we will use the matrix2 information from the oldest member transport. + return { ...t, useMatrix2: oldestMember.kind === "rtc" }; + }), first((t) => t != null && isLivekitTransport(t)), ), null, @@ -87,12 +98,24 @@ export const createLocalTransport$ = ({ * * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ - const preferredTransport$: Behavior = scope.behavior( - customLivekitUrl.value$.pipe( - switchMap((customUrl) => from(makeTransport(client, roomId, customUrl))), - ), - null, - ); + const preferredTransport$: Behavior = + scope.behavior( + combineLatest([customLivekitUrl.value$, useMatrix2$, delayId$]).pipe( + switchMap(([customUrl, useMatrix2, delayId]) => + from( + makeTransport( + client, + ownMembershipIdentity, + roomId, + customUrl, + useMatrix2, + delayId ?? undefined, + ), + ), + ), + ), + null, + ); /** * The chosen transport we should advertise in our MatrixRTC membership. @@ -123,10 +146,13 @@ const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ async function makeTransport( - client: Pick & OpenIDClientParts, + client: Pick & OpenIDClientParts, + membership: CallMembershipIdentityParts, roomId: string, urlFromDevSettings: string | null, -): Promise { + matrix2jwt = false, + delayId?: string, +): Promise { let transport: LivekitTransport | undefined; logger.trace("Searching for a preferred transport"); //TODO refactor this to use the jwt service returned alias. @@ -176,13 +202,18 @@ async function makeTransport( transport = transportFromConf; } - if (!transport) throw new MatrixRTCTransportMissingError(domain ?? ""); // this will call the jwt/sfu/get endpoint to pre create the livekit room. + if (!transport) throw new MatrixRTCTransportMissingError(domain ?? ""); + // this will call the jwt/sfu/get endpoint to pre create the livekit room. await getSFUConfigWithOpenID( client, + membership, transport.livekit_service_url, transport.livekit_alias, + matrix2jwt, + client.baseUrl, + delayId, ); - return transport; + return { ...transport, useMatrix2: matrix2jwt }; } diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 30c934b9..533f451a 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -27,7 +27,6 @@ import EventEmitter from "events"; import { type IOpenIDToken } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; -import type { LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { Connection, ConnectionState, @@ -39,7 +38,8 @@ import { ElementCallError, FailToGetOpenIdToken, } from "../../../utils/errors.ts"; -import { mockRemoteParticipant } from "../../../utils/test.ts"; +import { mockRemoteParticipant, ownMemberMock } from "../../../utils/test.ts"; +import { type LivekitTransportWithVersion } from "./ConnectionManager.ts"; let testScope: ObservableScope; @@ -50,10 +50,11 @@ let fakeLivekitRoom: MockedObject; let localParticipantEventEmiter: EventEmitter; let fakeLocalParticipant: MockedObject; -const livekitFocus: LivekitTransport = { +const livekitFocus: LivekitTransportWithVersion = { livekit_alias: "!roomID:example.org", livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt", type: "livekit", + useMatrix2: false, }; function setupTest(): void { @@ -137,7 +138,7 @@ function setupRemoteConnection(): Connection { return Promise.resolve(); }); - return new Connection(opts, logger); + return new Connection(opts, logger, ownMemberMock); } afterEach(() => { @@ -156,7 +157,7 @@ describe("Start connection states", () => { scope: testScope, livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new Connection(opts, logger); + const connection = new Connection(opts, logger, ownMemberMock); expect(connection.state$.getValue()).toEqual("Initialized"); }); @@ -172,7 +173,7 @@ describe("Start connection states", () => { livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new Connection(opts, logger); + const connection = new Connection(opts, logger, ownMemberMock); const capturedStates: (ConnectionState | Error)[] = []; const s = connection.state$.subscribe((value) => { @@ -222,7 +223,7 @@ describe("Start connection states", () => { livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new Connection(opts, logger); + const connection = new Connection(opts, logger, ownMemberMock); const capturedStates: (ConnectionState | Error)[] = []; const s = connection.state$.subscribe((value) => { @@ -279,7 +280,7 @@ describe("Start connection states", () => { livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new Connection(opts, logger); + const connection = new Connection(opts, logger, ownMemberMock); const capturedStates: (ConnectionState | Error)[] = []; const s = connection.state$.subscribe((value) => { diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index 05d0ec9e..d32bbce6 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -18,6 +18,7 @@ import { import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { BehaviorSubject, map } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { getSFUConfigWithOpenID, @@ -35,7 +36,7 @@ import { export interface ConnectionOpts { /** The media transport to connect to. */ - transport: LivekitTransport; + transport: LivekitTransport & { useMatrix2: boolean }; /** The Matrix client to use for OpenID and SFU config requests. */ client: OpenIDClientParts; /** The observable scope to use for this connection. */ @@ -88,7 +89,7 @@ export class Connection { /** * The media transport to connect to. */ - public readonly transport: LivekitTransport; + public readonly transport: LivekitTransport & { useMatrix2: boolean }; public readonly livekitRoom: LivekitRoom; @@ -189,9 +190,18 @@ export class Connection { protected async getSFUConfigWithOpenID(): Promise { return await getSFUConfigWithOpenID( this.client, + this.ownMembershipIdentity, this.transport.livekit_service_url, this.transport.livekit_alias, + this.transport.useMatrix2, ); + // client: OpenIDClientParts, + // membership: CallMembershipIdentityParts, + // serviceUrl: string, + // livekitRoomAlias: string, + // matrix2jwt: boolean, + // delayEndpointBaseUrl?: string, + // delayId?: string, } /** @@ -220,7 +230,11 @@ export class Connection { * * @param logger */ - public constructor(opts: ConnectionOpts, logger: Logger) { + public constructor( + opts: ConnectionOpts, + logger: Logger, + private ownMembershipIdentity: CallMembershipIdentityParts, + ) { this.logger = logger.getChild("[Connection]"); this.logger.info( `[Connection] Creating new connection to ${opts.transport.livekit_service_url} ${opts.transport.livekit_alias}`, diff --git a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts index 7c3a9eab..82a1a78a 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts @@ -5,7 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { Room as LivekitRoom, type RoomOptions, @@ -15,6 +14,7 @@ import { } from "livekit-client"; import { type Logger } from "matrix-js-sdk/lib/logger"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type ObservableScope } from "../../ObservableScope.ts"; import { Connection } from "./Connection.ts"; @@ -23,13 +23,15 @@ import type { MediaDevices } from "../../MediaDevices.ts"; import type { Behavior } from "../../Behavior.ts"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import { defaultLiveKitOptions } from "../../../livekit/options.ts"; +import { type LivekitTransportWithVersion } from "./ConnectionManager.ts"; // TODO evaluate if this should be done like the Publisher Factory export interface ConnectionFactory { createConnection( - transport: LivekitTransport, + transport: LivekitTransportWithVersion, scope: ObservableScope, logger: Logger, + ownMembershipIdentity: CallMembershipIdentityParts, ): Connection; } @@ -77,10 +79,19 @@ export class ECConnectionFactory implements ConnectionFactory { this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory; } + /** + * + * @param transport + * @param scope + * @param logger + * @param ownMembershipIdentity required to connect (using the jwt service) with the SFU. + * @returns + */ public createConnection( - transport: LivekitTransport, + transport: LivekitTransportWithVersion, scope: ObservableScope, logger: Logger, + ownMembershipIdentity: CallMembershipIdentityParts, ): Connection { return new Connection( { @@ -90,6 +101,7 @@ export class ECConnectionFactory implements ConnectionFactory { livekitRoomFactory: this.livekitRoomFactory, }, logger, + ownMembershipIdentity, ); } } diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts index 70bfb4de..4ab91646 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts @@ -14,26 +14,29 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { Epoch, mapEpoch, ObservableScope } from "../../ObservableScope.ts"; import { createConnectionManager$, + type LivekitTransportWithVersion, type ConnectionManagerData, } from "./ConnectionManager.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type Connection } from "./Connection.ts"; -import { withTestScheduler } from "../../../utils/test.ts"; +import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts"; import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { type Behavior } from "../../Behavior.ts"; // Some test constants -const TRANSPORT_1: LivekitTransport = { +const TRANSPORT_1: LivekitTransportWithVersion = { type: "livekit", livekit_service_url: "https://lk.example.org", livekit_alias: "!alias:example.org", + useMatrix2: false, }; -const TRANSPORT_2: LivekitTransport = { +const TRANSPORT_2: LivekitTransportWithVersion = { type: "livekit", livekit_service_url: "https://lk.sample.com", livekit_alias: "!alias:sample.com", + useMatrix2: false, }; let fakeConnectionFactory: ConnectionFactory; @@ -80,6 +83,7 @@ describe("connections$ stream", () => { a: new Epoch([TRANSPORT_1, TRANSPORT_2], 0), }), logger: logger, + ownMembershipIdentity: ownMemberMock, }); expectObservable( @@ -124,6 +128,7 @@ describe("connections$ stream", () => { f: new Epoch([TRANSPORT_1, TRANSPORT_2], 5), }), logger: logger, + ownMembershipIdentity: ownMemberMock, }); expectObservable( @@ -166,6 +171,7 @@ describe("connections$ stream", () => { c: new Epoch([TRANSPORT_1], 2), }), logger: logger, + ownMembershipIdentity: ownMemberMock, }); expectObservable( @@ -279,6 +285,7 @@ describe("connectionManagerData$ stream", () => { a: new Epoch([TRANSPORT_1, TRANSPORT_2], 0), }), logger, + ownMembershipIdentity: ownMemberMock, }); expectObservable(connectionManagerData$).toBe("abcd", { diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index 4303d50a..d5852d84 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -10,6 +10,7 @@ import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { combineLatest, map, of, switchMap, tap } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type RemoteParticipant } from "livekit-client"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type Behavior } from "../../Behavior.ts"; import { type Connection } from "./Connection.ts"; @@ -18,6 +19,10 @@ import { generateItemsWithEpoch } from "../../../utils/observable.ts"; import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts"; +export type LivekitTransportWithVersion = LivekitTransport & { + useMatrix2: boolean; +}; + export class ConnectionManagerData { private readonly store: Map = new Map(); @@ -59,8 +64,9 @@ export class ConnectionManagerData { interface Props { scope: ObservableScope; connectionFactory: ConnectionFactory; - inputTransports$: Behavior>; + inputTransports$: Behavior>; logger: Logger; + ownMembershipIdentity: CallMembershipIdentityParts; } // TODO - write test for scopes (do we really need to bind scope) @@ -87,6 +93,7 @@ export function createConnectionManager$({ connectionFactory, inputTransports$, logger: parentLogger, + ownMembershipIdentity, }: Props): IConnectionManager { const logger = parentLogger.getChild("[ConnectionManager]"); // TODO logger: only construct one logger from the client and make it compatible via a EC specific sing @@ -119,20 +126,26 @@ export function createConnectionManager$({ function* (transports) { for (const transport of transports) yield { - keys: [transport.livekit_service_url, transport.livekit_alias], + keys: [ + transport.livekit_service_url, + transport.livekit_alias, + transport.useMatrix2, + ], data: undefined, }; }, - (scope, _data$, serviceUrl, alias) => { + (scope, _data$, serviceUrl, alias, useMatrix2) => { logger.debug(`Creating connection to ${serviceUrl} (${alias})`); const connection = connectionFactory.createConnection( { type: "livekit", livekit_service_url: serviceUrl, livekit_alias: alias, + useMatrix2, }, scope, logger, + ownMembershipIdentity, ); // Start the connection immediately // Use connection state to track connection progress @@ -187,12 +200,12 @@ export function createConnectionManager$({ return { connectionManagerData$ }; } -function removeDuplicateTransports( - transports: LivekitTransport[], -): LivekitTransport[] { +function removeDuplicateTransports( + transports: T[], +): T[] { return transports.reduce((acc, transport) => { if (!acc.some((t) => areLivekitTransportsEqual(t, transport))) acc.push(transport); return acc; - }, [] as LivekitTransport[]); + }, [] as T[]); } diff --git a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts index 0c439a6b..3c60e776 100644 --- a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts +++ b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts @@ -15,7 +15,11 @@ import EventEmitter from "events"; import { ObservableScope } from "../../ObservableScope.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts"; import type { OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; -import { exampleTransport, mockMediaDevices } from "../../../utils/test.ts"; +import { + exampleTransport, + mockMediaDevices, + ownMemberMock, +} from "../../../utils/test.ts"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import { constant } from "../../Behavior"; @@ -72,7 +76,12 @@ describe("ECConnectionFactory - Audio inputs options", () => { echo, noise, ); - ecConnectionFactory.createConnection(exampleTransport, testScope, logger); + ecConnectionFactory.createConnection( + exampleTransport, + testScope, + logger, + ownMemberMock, + ); // Check if Room was constructed with expected options expect(RoomConstructor).toHaveBeenCalledWith( @@ -113,7 +122,12 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => { false, false, ); - ecConnectionFactory.createConnection(exampleTransport, testScope, logger); + ecConnectionFactory.createConnection( + exampleTransport, + testScope, + logger, + ownMemberMock, + ); // Check if Room was constructed with expected options expect(RoomConstructor).toHaveBeenCalledWith( diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index 0c61ba06..9178c347 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -176,9 +176,9 @@ export function createMatrixLivekitMembers$({ // TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$) // TODO add this to the JS-SDK -export function areLivekitTransportsEqual( - t1: LivekitTransport | null, - t2: LivekitTransport | null, +export function areLivekitTransportsEqual( + t1: T | null, + t2: T | null, ): boolean { if (t1 && t2) return t1.livekit_service_url === t2.livekit_service_url; // In case we have different lk rooms in the same SFU (depends on the livekit authorization service) diff --git a/src/state/CallViewModel/remoteMembers/integration.test.ts b/src/state/CallViewModel/remoteMembers/integration.test.ts index 202b3f56..c9e02a7c 100644 --- a/src/state/CallViewModel/remoteMembers/integration.test.ts +++ b/src/state/CallViewModel/remoteMembers/integration.test.ts @@ -24,6 +24,7 @@ import { mockCallMembership, mockComputeLivekitParticipantIdentity$, mockMediaDevices, + ownMemberMock, withTestScheduler, } from "../../../utils/test.ts"; import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; @@ -128,6 +129,7 @@ test("bob, carl, then bob joining no tracks yet", () => { connectionFactory: ecConnectionFactory, inputTransports$: membershipsAndTransports.transports$, logger: logger, + ownMembershipIdentity: ownMemberMock, }); const matrixLivekitItems$ = createMatrixLivekitMembers$({ diff --git a/src/state/SessionBehaviors.ts b/src/state/SessionBehaviors.ts index e174a1cc..b61d2fe6 100644 --- a/src/state/SessionBehaviors.ts +++ b/src/state/SessionBehaviors.ts @@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details. import { type CallMembership, isLivekitTransport, - type LivekitTransport, type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; @@ -21,15 +20,18 @@ import { type ObservableScope, } from "./ObservableScope"; import { type Behavior } from "./Behavior"; +import { type LivekitTransportWithVersion } from "./CallViewModel/remoteMembers/ConnectionManager"; export const membershipsAndTransports$ = ( scope: ObservableScope, memberships$: Behavior>, ): { membershipsWithTransport$: Behavior< - Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> + Epoch< + { membership: CallMembership; transport?: LivekitTransportWithVersion }[] + > >; - transports$: Behavior>; + transports$: Behavior>; } => { /** * Lists the transports used by ourselves, plus all other MatrixRTC session @@ -47,7 +49,12 @@ export const membershipsAndTransports$ = ( const transport = membership.getTransport(oldestMembership); return { membership, - transport: isLivekitTransport(transport) ? transport : undefined, + transport: isLivekitTransport(transport) + ? { + ...transport, + useMatrix2: membership.kind === "rtc", + } + : undefined, }; }); }), diff --git a/src/utils/test.ts b/src/utils/test.ts index 7d251640..968b7160 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -25,7 +25,6 @@ import { import { CallMembership, type LivekitFocusSelection, - type LivekitTransport, type MatrixRTCSession, MatrixRTCSessionEvent, type MatrixRTCSessionEventHandlerMap, @@ -67,6 +66,7 @@ import { type MediaDevices } from "../state/MediaDevices"; import { type Behavior, constant } from "../state/Behavior"; import { ObservableScope } from "../state/ObservableScope"; import { MuteStates } from "../state/MuteStates"; +import { type LivekitTransportWithVersion } from "../state/CallViewModel/remoteMembers/ConnectionManager"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); @@ -197,10 +197,11 @@ export function mockEmitter(): EmitterMock { }; } -export const exampleTransport: LivekitTransport = { +export const exampleTransport: LivekitTransportWithVersion = { type: "livekit", livekit_service_url: "https://lk.example.org", livekit_alias: "!alias:example.org", + useMatrix2: false, }; export function mockCallMembership( @@ -256,6 +257,11 @@ export function mockRtcMembership( return cms; } +export const ownMemberMock: CallMembershipIdentityParts = { + userId: "@alice:example.org", + deviceId: "DEVICE", + memberId: "@alice:example.org:DEVICE", +}; // Maybe it'd be good to move this to matrix-js-sdk? Our testing needs are // rather simple, but if one util to mock a member is good enough for us, maybe // it's useful for matrix-js-sdk consumers in general. diff --git a/yarn.lock b/yarn.lock index db1db491..a3211330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10338,9 +10338,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00": - version: 39.3.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00" +"matrix-js-sdk@portal:/Users/timo/Projects/matrix-js-sdk::locator=element-call%40workspace%3A.": + version: 0.0.0-use.local + resolution: "matrix-js-sdk@portal:/Users/timo/Projects/matrix-js-sdk::locator=element-call%40workspace%3A." dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" @@ -10356,9 +10356,8 @@ __metadata: sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/9607b0c063c873a24c1a2d05cc7500d60c32556ec82b666ebaae5c5e829faf5bb7639780efddea7211e6b9873098bd53b97656f041e932e8b0de0c208ccabbff languageName: node - linkType: hard + linkType: soft "matrix-widget-api@npm:^1.14.0": version: 1.15.0 From 50f3bf00aee60fde10012c8999599346819f87c0 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 17 Dec 2025 10:22:46 +0100 Subject: [PATCH 09/76] use correct js-sdk --- package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 35589771..f34fab5f 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=9779ac975df2f296958e3c4be254fa46ebd67ea4", "matrix-widget-api": "^1.14.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/yarn.lock b/yarn.lock index a3211330..b83976a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7533,7 +7533,7 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=f5f1b8efb46b3d55a7eebfabb4a61496640b8b00" + matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=9779ac975df2f296958e3c4be254fa46ebd67ea4" matrix-widget-api: "npm:^1.14.0" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -10338,9 +10338,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@portal:/Users/timo/Projects/matrix-js-sdk::locator=element-call%40workspace%3A.": - version: 0.0.0-use.local - resolution: "matrix-js-sdk@portal:/Users/timo/Projects/matrix-js-sdk::locator=element-call%40workspace%3A." +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=9779ac975df2f296958e3c4be254fa46ebd67ea4": + version: 39.3.0 + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=9779ac975df2f296958e3c4be254fa46ebd67ea4" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" @@ -10356,8 +10356,9 @@ __metadata: sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" + checksum: 10c0/78c27847b58c229513bd28c4c4ad391d8af6722711d3d0f42e93a537d7a827a7233e920936dd8d7005c7893bad17a503c3f62b56ecfed3cf4ae81a5097b4ac21 languageName: node - linkType: soft + linkType: hard "matrix-widget-api@npm:^1.14.0": version: 1.15.0 From 55d18f10fe45cd78c528f46b96db8eecabc7c9ce Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 19 Dec 2025 19:23:41 +0100 Subject: [PATCH 10/76] temp refactored membership rtcidentity --- package.json | 2 +- src/e2ee/matrixKeyProvider.ts | 110 +++++++++++------- src/state/CallViewModel/CallViewModel.test.ts | 9 -- .../LivekitParticipantIdentity.ts | 53 --------- .../remoteMembers/MatrixLivekitMembers.ts | 57 ++------- .../remoteMembers/integration.test.ts | 6 - src/utils/test.ts | 3 +- yarn.lock | 8 +- 8 files changed, 87 insertions(+), 161 deletions(-) delete mode 100644 src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts diff --git a/package.json b/package.json index f34fab5f..6c74b84c 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=9779ac975df2f296958e3c4be254fa46ebd67ea4", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725", "matrix-widget-api": "^1.14.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index a499f45c..166fd82c 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -6,18 +6,13 @@ Please see LICENSE in the repository root for full details. */ import { BaseKeyProvider } from "livekit-client"; -import { logger } from "matrix-js-sdk/lib/logger"; import { type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; -import { firstValueFrom } from "rxjs"; - -import { - computeLivekitParticipantIdentity$, - livekitIdentityInput, -} from "../state/CallViewModel/remoteMembers/LivekitParticipantIdentity"; +import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +const logger = rootLogger.getChild("[MatrixKeyProvider]"); export class MatrixKeyProvider extends BaseKeyProvider { private rtcSession?: MatrixRTCSession; @@ -32,6 +27,10 @@ export class MatrixKeyProvider extends BaseKeyProvider { MatrixRTCSessionEvent.EncryptionKeyChanged, this.onEncryptionKeyChanged, ); + this.rtcSession.off( + MatrixRTCSessionEvent.MembershipsChanged, + this.onMembershipsChanged, + ); } this.rtcSession = rtcSession; @@ -40,55 +39,86 @@ export class MatrixKeyProvider extends BaseKeyProvider { MatrixRTCSessionEvent.EncryptionKeyChanged, this.onEncryptionKeyChanged, ); + this.rtcSession.on( + MatrixRTCSessionEvent.MembershipsChanged, + this.onMembershipsChanged, + ); // The new session could be aware of keys of which the old session wasn't, // so emit key changed events this.rtcSession.reemitEncryptionKeys(); } + private keyCache = new Array<{ + membership: CallMembershipIdentityParts; + encryptionKey: Uint8Array; + encryptionKeyIndex: number; + }>(); + + private onMembershipsChanged = (): void => { + const duplicatedArray = this.keyCache; + // Reset key cache first. It will get repopulated when calling `onEncryptionKeyChanged` + this.keyCache = []; + let next = duplicatedArray.pop(); + while (next !== undefined) { + logger.debug( + "[KeyCache] remove key event from the cache and try adding it again. For membership: ", + next.membership, + ); + this.onEncryptionKeyChanged( + next.encryptionKey, + next.encryptionKeyIndex, + next.membership, + ); + next = duplicatedArray.pop(); + } + }; + private onEncryptionKeyChanged = ( encryptionKey: Uint8Array, encryptionKeyIndex: number, membership: CallMembershipIdentityParts, ): void => { - const unhashedIdentity = livekitIdentityInput(membership); - // This is the only way we can get the kind of the membership event we just received the key for. // best case we want to recompute this once the memberships change (you can receive the key before the participant...) - // - // TODO change this to `?? "rtc"` for newer versions. - const kind = - this.rtcSession?.memberships.find( - (m) => - m.userId === membership.userId && - m.deviceId === membership.deviceId && - m.memberId === membership.memberId, - )?.kind ?? "session"; + const membershipFull = this.rtcSession?.memberships.find( + (m) => + m.userId === membership.userId && + m.deviceId === membership.deviceId && + m.memberId === membership.memberId, + ); + if (!membershipFull) { + logger.debug( + "[KeyCache] Added key event to the cache because we do not have a membership for it (yet): ", + membership, + ); + this.keyCache.push({ membership, encryptionKey, encryptionKeyIndex }); + return; + } - Promise.all([ - crypto.subtle.importKey("raw", encryptionKey, "HKDF", false, [ + crypto.subtle + .importKey("raw", encryptionKey, "HKDF", false, [ "deriveBits", "deriveKey", - ]), - firstValueFrom(computeLivekitParticipantIdentity$(membership, kind)), - ]).then( - ([keyMaterial, livekitParticipantId]) => { - this.onSetEncryptionKey( - keyMaterial, - livekitParticipantId, - encryptionKeyIndex, - ); + ]) + .then( + (keyMaterial) => { + this.onSetEncryptionKey( + keyMaterial, + membershipFull.rtcBackendIdentity, + encryptionKeyIndex, + ); - logger.debug( - `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${livekitParticipantId} (before hash: ${unhashedIdentity}) encryptionKeyIndex=${encryptionKeyIndex}`, - ); - }, - (e) => { - logger.error( - `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${unhashedIdentity} encryptionKeyIndex=${encryptionKeyIndex}`, - e, - ); - }, - ); + logger.debug( + `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${membershipFull.rtcBackendIdentity} (before hash: ${membershipFull.userId}) encryptionKeyIndex=${encryptionKeyIndex}`, + ); + }, + (e) => { + logger.error( + `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipFull.userId} encryptionKeyIndex=${encryptionKeyIndex}`, + e, + ); + }, + ); }; } diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 505cb19f..6e3837c4 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -44,7 +44,6 @@ import { mockRtcMembership, testScope, exampleTransport, - mockComputeLivekitParticipantIdentity$, } from "../../utils/test.ts"; import { E2eeType } from "../../e2ee/e2eeType.ts"; import { @@ -88,14 +87,6 @@ vi.mock( }), ); -vi.mock( - import("./remoteMembers/LivekitParticipantIdentity.ts"), - async (importOriginal) => ({ - ...(await importOriginal()), - computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, - }), -); - const yesNo = { y: true, n: false, diff --git a/src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts b/src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts deleted file mode 100644 index e43d0bd1..00000000 --- a/src/state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { encodeUnpaddedBase64Url } from "matrix-js-sdk"; -import { sha256 } from "matrix-js-sdk/lib/digest"; -import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; -import { from, type Observable } from "rxjs"; - -const livekitParticipantIdentityCache = new Map(); - -/** - * The string that is computed based on the membership and used for the computing the hash. - * `${userId}:${deviceId}:${membershipID}` - * as the direct imput for: await sha256(input) - */ -export const livekitIdentityInput = ({ - userId, - deviceId, - memberId, -}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`; - -export function computeLivekitParticipantIdentity$( - membership: CallMembershipIdentityParts, - kind: "rtc" | "session", -): Observable { - const compute = async (): Promise => { - switch (kind) { - case "rtc": { - const input = livekitIdentityInput(membership); - if (livekitParticipantIdentityCache.size > 400) - // prevent memory leaks in a stupid/simple way - livekitParticipantIdentityCache.clear(); - // TODO use non deprecated memberId - if (livekitParticipantIdentityCache.has(input)) - return livekitParticipantIdentityCache.get(input)!; - else { - const hashBuffer = await sha256(input); - const hashedString = encodeUnpaddedBase64Url(hashBuffer); - livekitParticipantIdentityCache.set(input, hashedString); - return hashedString; - } - } - case "session": - default: - return `${membership.userId}:${membership.deviceId}`; - } - }; - return from(compute()); -} diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index 9178c347..a5d6b2f6 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -10,7 +10,7 @@ import { type LivekitTransport, type CallMembership, } from "matrix-js-sdk/lib/matrixrtc"; -import { combineLatest, filter, map, switchMap } from "rxjs"; +import { combineLatest, filter, map } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { type Behavior } from "../../Behavior"; @@ -18,7 +18,6 @@ import { type IConnectionManager } from "./ConnectionManager"; import { Epoch, type ObservableScope } from "../../ObservableScope"; import { type Connection } from "./Connection"; import { generateItemsWithEpoch } from "../../../utils/observable"; -import { computeLivekitParticipantIdentity$ } from "./LivekitParticipantIdentity"; const logger = rootLogger.getChild("[MatrixLivekitMembers]"); @@ -82,45 +81,12 @@ export function createMatrixLivekitMembers$({ membershipsWithTransport$, connectionManager, }: Props): Behavior> { - /** - * This internal observable is used to compute the async sha256 hash of the user's identity. - * a promise is treated like an observable. So we can switchMap on the promise from the identity computation. - * The last update to `membershipsWithTransport$` will always be the last promise we pass to switchMap. - * So we will eventually always end up with the latest memberships and their identities. - */ - const membershipsWithTransportAndLivekitIdentity$ = - membershipsWithTransport$.pipe( - switchMap((membershipsWithTransport) => { - const { value, epoch } = membershipsWithTransport; - const membershipsWithTransportAndLkIdentityPromises = value.map( - (obj) => { - return computeLivekitParticipantIdentity$( - obj.membership, - obj.membership.kind, - ); - }, - ); - return combineLatest( - membershipsWithTransportAndLkIdentityPromises, - ).pipe( - map((identities) => { - const membershipsWithTransportAndLkIdentity = value.map( - ({ transport, membership }, index) => { - return { transport, membership, identity: identities[index] }; - }, - ); - return new Epoch(membershipsWithTransportAndLkIdentity, epoch); - }), - ); - }), - ); - /** * Stream of all the call members and their associated livekit data (if available). */ return scope.behavior( combineLatest([ - membershipsWithTransportAndLivekitIdentity$, + membershipsWithTransport$, connectionManager.connectionManagerData$, ]).pipe( filter((values) => @@ -131,37 +97,34 @@ export function createMatrixLivekitMembers$({ // Generator function. // creates an array of `{key, data}[]` // Each change in the keys (new key, missing key) will result in a call to the factory function. - function* ([membershipsWithTransportAndLivekitIdentity, managerData]) { - for (const { - membership, - transport, - identity, - } of membershipsWithTransportAndLivekitIdentity) { + function* ([membershipsWithTransport, managerData]) { + for (const { membership, transport } of membershipsWithTransport) { const participants = transport ? managerData.getParticipantForTransport(transport) : []; const participant = - participants.find((p) => p.identity == identity) ?? null; + participants.find( + (p) => p.identity == membership.rtcBackendIdentity, + ) ?? null; const connection = transport ? managerData.getConnectionForTransport(transport) : null; yield { - keys: [identity, membership.userId, membership.deviceId], + keys: [membership.userId, membership.deviceId], data: { membership, participant, connection }, }; } }, // Each update where the key of the generator array do not change will result in updates to the `data$` observable in the factory. - (scope, data$, identity, userId, deviceId) => { + (scope, data$, userId, deviceId) => { logger.debug( - `Generating member for livekitIdentity: ${identity}, userId:deviceId: ${userId}${deviceId}`, + `Generating member for livekitIdentity: ${data$.value.membership.rtcBackendIdentity}, userId:deviceId: ${userId}${deviceId}`, ); const { participant$, ...rest } = scope.splitBehavior(data$); // will only get called once per `participantId, userId` pair. // updates to data$ and as a result to displayName$ and mxcAvatarUrl$ are more frequent. return { - identity, userId, participant: { type: "remote" as const, value$: participant$ }, ...rest, diff --git a/src/state/CallViewModel/remoteMembers/integration.test.ts b/src/state/CallViewModel/remoteMembers/integration.test.ts index c9e02a7c..00062c60 100644 --- a/src/state/CallViewModel/remoteMembers/integration.test.ts +++ b/src/state/CallViewModel/remoteMembers/integration.test.ts @@ -22,7 +22,6 @@ import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import { mockCallMembership, - mockComputeLivekitParticipantIdentity$, mockMediaDevices, ownMemberMock, withTestScheduler, @@ -45,11 +44,6 @@ let lkRoomFactory: () => LivekitRoom; const createdMockLivekitRooms: Map = new Map(); -vi.mock(import("./LivekitParticipantIdentity.ts"), async (importOriginal) => ({ - ...(await importOriginal()), - computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, -})); - beforeEach(() => { testScope = new ObservableScope(); mockClient = { diff --git a/src/utils/test.ts b/src/utils/test.ts index 968b7160..56148b32 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -252,7 +252,8 @@ export function mockRtcMembership( content: data, }); - const cms = new CallMembership(event, data); + const membershipData = CallMembership.membershipDataFromMatrixEvent(event); + const cms = new CallMembership(event, membershipData, "xx"); vi.mocked(cms).getTransport = vi.fn().mockReturnValue(fociPreferred[0]); return cms; } diff --git a/yarn.lock b/yarn.lock index b83976a2..c4b2e957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7533,7 +7533,7 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=9779ac975df2f296958e3c4be254fa46ebd67ea4" + matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725" matrix-widget-api: "npm:^1.14.0" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -10338,9 +10338,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=9779ac975df2f296958e3c4be254fa46ebd67ea4": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725": version: 39.3.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=9779ac975df2f296958e3c4be254fa46ebd67ea4" + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" @@ -10356,7 +10356,7 @@ __metadata: sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/78c27847b58c229513bd28c4c4ad391d8af6722711d3d0f42e93a537d7a827a7233e920936dd8d7005c7893bad17a503c3f62b56ecfed3cf4ae81a5097b4ac21 + checksum: 10c0/2e7061f6e648c91aaeb30b3e01626d855e24efcb330bbe432fcba199bd46b0b0d998cbc545748e1c72a7b643d25581f988fcad9bbaa42912a6ec96a27c41d0de languageName: node linkType: hard From 3ba2d13e2771dae0ca1e4687b690c6661f669f9a Mon Sep 17 00:00:00 2001 From: Timo K Date: Sun, 28 Dec 2025 21:04:46 +0100 Subject: [PATCH 11/76] use the js-sdk where the hashed rtcSessionIdeintity is already part of the event (no need to compute it in the encryption manager) --- package.json | 2 +- src/e2ee/matrixKeyProvider.ts | 55 ++--------------------------------- yarn.lock | 10 +++---- 3 files changed, 8 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 6c74b84c..2611587a 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4", "matrix-widget-api": "^1.14.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index 166fd82c..962c9bc6 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -10,7 +10,6 @@ import { type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; -import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; const logger = rootLogger.getChild("[MatrixKeyProvider]"); @@ -27,10 +26,6 @@ export class MatrixKeyProvider extends BaseKeyProvider { MatrixRTCSessionEvent.EncryptionKeyChanged, this.onEncryptionKeyChanged, ); - this.rtcSession.off( - MatrixRTCSessionEvent.MembershipsChanged, - this.onMembershipsChanged, - ); } this.rtcSession = rtcSession; @@ -39,63 +34,17 @@ export class MatrixKeyProvider extends BaseKeyProvider { MatrixRTCSessionEvent.EncryptionKeyChanged, this.onEncryptionKeyChanged, ); - this.rtcSession.on( - MatrixRTCSessionEvent.MembershipsChanged, - this.onMembershipsChanged, - ); // The new session could be aware of keys of which the old session wasn't, // so emit key changed events this.rtcSession.reemitEncryptionKeys(); } - private keyCache = new Array<{ - membership: CallMembershipIdentityParts; - encryptionKey: Uint8Array; - encryptionKeyIndex: number; - }>(); - - private onMembershipsChanged = (): void => { - const duplicatedArray = this.keyCache; - // Reset key cache first. It will get repopulated when calling `onEncryptionKeyChanged` - this.keyCache = []; - let next = duplicatedArray.pop(); - while (next !== undefined) { - logger.debug( - "[KeyCache] remove key event from the cache and try adding it again. For membership: ", - next.membership, - ); - this.onEncryptionKeyChanged( - next.encryptionKey, - next.encryptionKeyIndex, - next.membership, - ); - next = duplicatedArray.pop(); - } - }; - private onEncryptionKeyChanged = ( encryptionKey: Uint8Array, encryptionKeyIndex: number, - membership: CallMembershipIdentityParts, + rtcBackendIdentity: string, ): void => { - // This is the only way we can get the kind of the membership event we just received the key for. - // best case we want to recompute this once the memberships change (you can receive the key before the participant...) - const membershipFull = this.rtcSession?.memberships.find( - (m) => - m.userId === membership.userId && - m.deviceId === membership.deviceId && - m.memberId === membership.memberId, - ); - if (!membershipFull) { - logger.debug( - "[KeyCache] Added key event to the cache because we do not have a membership for it (yet): ", - membership, - ); - this.keyCache.push({ membership, encryptionKey, encryptionKeyIndex }); - return; - } - crypto.subtle .importKey("raw", encryptionKey, "HKDF", false, [ "deriveBits", @@ -105,7 +54,7 @@ export class MatrixKeyProvider extends BaseKeyProvider { (keyMaterial) => { this.onSetEncryptionKey( keyMaterial, - membershipFull.rtcBackendIdentity, + rtcBackendIdentity, encryptionKeyIndex, ); diff --git a/yarn.lock b/yarn.lock index c4b2e957..0ddaad61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7533,7 +7533,7 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725" + matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4" matrix-widget-api: "npm:^1.14.0" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -10338,9 +10338,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725": - version: 39.3.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=2bb3b03a248e689f7460f4e70d5ffbf10353c725" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4": + version: 39.4.0 + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" @@ -10356,7 +10356,7 @@ __metadata: sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/2e7061f6e648c91aaeb30b3e01626d855e24efcb330bbe432fcba199bd46b0b0d998cbc545748e1c72a7b643d25581f988fcad9bbaa42912a6ec96a27c41d0de + checksum: 10c0/2375dd3d9191f78fe589b0d3170f3da7792ed469a81d3ba3cd12f4915fd33a859f8af3491edb9cf0cdaa1f881a3ea7c1bf7539e850ad0360ec9981271f462c81 languageName: node linkType: hard From 0f5c5d8be56d0f3bd522516cb7e693d4c879033a Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 29 Dec 2025 17:38:54 +0100 Subject: [PATCH 12/76] cleanup based on new js-sdk impl --- package.json | 2 +- src/e2ee/matrixKeyProvider.ts | 6 +- src/livekit/openIDSFU.ts | 37 +++++++--- src/room/CallEventAudioRenderer.test.tsx | 8 --- src/room/InCallView.test.tsx | 8 --- src/settings/settings.ts | 6 ++ src/state/CallViewModel/CallViewModel.ts | 40 +++++------ .../localMember/LocalTransport.test.ts | 8 +-- .../localMember/LocalTransport.ts | 65 +++++++++-------- .../remoteMembers/Connection.test.ts | 4 +- .../CallViewModel/remoteMembers/Connection.ts | 23 +++--- .../remoteMembers/ConnectionFactory.ts | 9 ++- .../remoteMembers/ConnectionManager.test.ts | 21 +++--- .../remoteMembers/ConnectionManager.ts | 72 +++++++++++++------ .../remoteMembers/integration.test.ts | 10 ++- src/state/SessionBehaviors.ts | 15 ++-- src/utils/test.ts | 5 +- yarn.lock | 8 +-- 18 files changed, 191 insertions(+), 156 deletions(-) diff --git a/package.json b/package.json index 2611587a..346c12cf 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=6f0815930a008eff8f86e6e5748d447be0e7c25e", "matrix-widget-api": "^1.14.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index 962c9bc6..a9b0865f 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -11,6 +11,7 @@ import { MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; const logger = rootLogger.getChild("[MatrixKeyProvider]"); export class MatrixKeyProvider extends BaseKeyProvider { @@ -43,6 +44,7 @@ export class MatrixKeyProvider extends BaseKeyProvider { private onEncryptionKeyChanged = ( encryptionKey: Uint8Array, encryptionKeyIndex: number, + membershipParts: CallMembershipIdentityParts, rtcBackendIdentity: string, ): void => { crypto.subtle @@ -59,12 +61,12 @@ export class MatrixKeyProvider extends BaseKeyProvider { ); logger.debug( - `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${membershipFull.rtcBackendIdentity} (before hash: ${membershipFull.userId}) encryptionKeyIndex=${encryptionKeyIndex}`, + `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${rtcBackendIdentity} (before hash: ${membershipParts.userId}) encryptionKeyIndex=${encryptionKeyIndex}`, ); }, (e) => { logger.error( - `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipFull.userId} encryptionKeyIndex=${encryptionKeyIndex}`, + `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipParts.userId} encryptionKeyIndex=${encryptionKeyIndex}`, e, ); }, diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index f07bb035..aaf07615 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -6,8 +6,8 @@ Please see LICENSE in the repository root for full details. */ import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk"; -import { logger } from "matrix-js-sdk/lib/logger"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; +import { type Logger } from "matrix-js-sdk/lib/logger"; import { FailToGetOpenIdToken } from "../utils/errors"; import { doNetworkOperationWithRetry } from "../utils/matrix"; @@ -28,8 +28,17 @@ export type OpenIDClientParts = Pick< * to the matrix RTC backend in order to get acces to the SFU. * It has built-in retry for calls to the homeserver with a backoff policy. * @param client + * @param membership * @param serviceUrl - * @param matrixRoomId + * @param forceOldEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination + * instead of a hash. + * This function by default uses whatever is possible with the current jwt service installed next to the SFU. + * For remote connections this does not matter, since we will not publish there we can rely on the newest option. + * For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events. + * @param livekitRoomAlias + * @param delayEndpointBaseUrl + * @param delayId + * @param logger * @returns Object containing the token information * @throws FailToGetOpenIdToken */ @@ -37,10 +46,11 @@ export async function getSFUConfigWithOpenID( client: OpenIDClientParts, membership: CallMembershipIdentityParts, serviceUrl: string, + forceOldJwtEndpoint: boolean, livekitRoomAlias: string, - matrix2jwt: boolean, delayEndpointBaseUrl?: string, delayId?: string, + logger?: Logger, ): Promise { let openIdToken: IOpenIDToken; try { @@ -52,26 +62,35 @@ export async function getSFUConfigWithOpenID( error instanceof Error ? error : new Error("Unknown error"), ); } - logger.debug("Got openID token", openIdToken); + logger?.debug("Got openID token", openIdToken); - logger.info(`Trying to get JWT for focus ${serviceUrl}...`); + logger?.info(`Trying to get JWT for focus ${serviceUrl}...`); const args: [CallMembershipIdentityParts, string, string, IOpenIDToken] = [ membership, serviceUrl, livekitRoomAlias, openIdToken, ]; - if (matrix2jwt) { + try { + // we do not want to try the old endpoint, since we are not sending the new matrix2.0 sticky events (no hashed identity in the event) + if (forceOldJwtEndpoint) throw new Error("Force old jwt endpoint"); + if (!delayId) + throw new Error("No delayId, Won't try matrix 2.0 jwt endpoint."); + const sfuConfig = await getLiveKitJWTWithDelayDelegation( ...args, delayEndpointBaseUrl, delayId, ); - logger.info(`Got JWT from call's active focus URL.`); + logger?.info(`Got JWT from call's active focus URL.`); return sfuConfig; - } else { + } catch (e) { + logger?.warn( + `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy)`, + e, + ); const sfuConfig = await getLiveKitJWT(...args); - logger.info(`Got JWT from call's active focus URL.`); + logger?.info(`Got JWT from call's active focus URL.`); return sfuConfig; } } diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 38f56b14..733346eb 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -23,7 +23,6 @@ import { import { exampleTransport, - mockComputeLivekitParticipantIdentity$, mockMatrixRoomMember, mockRtcMembership, } from "../utils/test"; @@ -48,13 +47,6 @@ vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({ ...(await importOriginal()), makeTransport: (): [LivekitTransport] => [exampleTransport], })); -vitest.mock( - import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"), - async (importOriginal) => ({ - ...(await importOriginal()), - computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, - }), -); afterEach(() => { vitest.clearAllMocks(); diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index cd0af547..8ac4bccb 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -23,7 +23,6 @@ import { TooltipProvider } from "@vector-im/compound-web"; import { RoomContext, useLocalParticipant } from "@livekit/components-react"; import { - mockComputeLivekitParticipantIdentity$, mockLivekitRoom, mockLocalParticipant, mockMatrixRoom, @@ -62,13 +61,6 @@ vi.mock("../livekit/MatrixAudioRenderer"); vi.mock("react-use-measure", () => ({ default: (): [() => void, object] => [(): void => {}, {}], })); -vi.mock( - import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"), - async (importOriginal) => ({ - ...(await importOriginal()), - computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$, - }), -); const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); const localParticipant = mockLocalParticipant({ diff --git a/src/settings/settings.ts b/src/settings/settings.ts index f85e1414..33408fd9 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -127,6 +127,12 @@ export const alwaysShowIphoneEarpiece = new Setting( export enum MatrixRTCMode { Legacy = "legacy", Compatibil = "compatibil", + /** This implies using + * - sticky events + * - hashed RTC backend identity + * - the new endpoint for the jwt token on the local membership (remote memberships will always try the new jwt endpoint first -> then the legacy one) + * - use the hashed identity for the local membership + */ Matrix_2_0 = "matrix_2_0", } diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 23c58268..922a390e 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -415,17 +415,18 @@ export function createCallViewModel$( const ownMembershipIdentity: CallMembershipIdentityParts = { userId, deviceId, + // TODO look into this!!! memberId: `${userId}:${deviceId}`, }; + const useOldJwtEndpoint$ = scope.behavior( + options.matrixRTCMode$.pipe(map((v) => v !== MatrixRTCMode.Matrix_2_0)), + ); const localTransport$ = createLocalTransport$({ scope: scope, memberships$: memberships$, ownMembershipIdentity, client, - useMatrix2$: scope.behavior( - options.matrixRTCMode$.pipe(map((v) => v === MatrixRTCMode.Matrix_2_0)), - ), delayId$: scope.behavior( ( fromEvent( @@ -436,6 +437,7 @@ export function createCallViewModel$( matrixRTCSession.delayId ?? null, ), roomId: matrixRoom.roomId, + useOldJwtEndpoint$, useOldestMember$: scope.behavior( options.matrixRTCMode$.pipe(map((v) => v === MatrixRTCMode.Legacy)), ), @@ -455,29 +457,19 @@ export function createCallViewModel$( const connectionManager = createConnectionManager$({ scope: scope, connectionFactory: connectionFactory, - inputTransports$: scope.behavior( - combineLatest( - [ - localTransport$.pipe( - catchError((e: unknown) => { - logger.info( - "dont pass local transport to createConnectionManager$. localTransport$ threw an error", - e, - ); - return of(null); - }), - ), - membershipsAndTransports.transports$, - ], - (localTransport, transports) => { - const localTransportAsArray = localTransport ? [localTransport] : []; - return transports.mapInner((transports) => [ - ...localTransportAsArray, - ...transports, - ]); - }, + localTransport$: scope.behavior( + localTransport$.pipe( + catchError((e: unknown) => { + logger.info( + "could not pass local transport to createConnectionManager$. localTransport$ threw an error", + e, + ); + return of(null); + }), ), ), + remoteTransports$: membershipsAndTransports.transports$, + forceOldJwtEndpointForLocalTransport$: useOldJwtEndpoint$, logger: logger, ownMembershipIdentity, }); diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index ba030757..e7df6e33 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -38,7 +38,7 @@ describe("LocalTransport", () => { getDeviceId: vi.fn(), }, ownMembershipIdentity: ownMemberMock, - useMatrix2$: constant(false), + useOldJwtEndpoint$: constant(false), delayId$: constant("delay_id_mock"), }); await flushPromises(); @@ -76,7 +76,7 @@ describe("LocalTransport", () => { getDeviceId: vi.fn(), }, ownMembershipIdentity: ownMemberMock, - useMatrix2$: constant(false), + useOldJwtEndpoint$: constant(false), delayId$: constant("delay_id_mock"), }); localTransport$.subscribe( @@ -116,7 +116,7 @@ describe("LocalTransport", () => { baseUrl: "https://lk.example.org", }, ownMembershipIdentity: ownMemberMock, - useMatrix2$: constant(false), + useOldJwtEndpoint$: constant(false), delayId$: constant("delay_id_mock"), }); @@ -155,7 +155,7 @@ describe("LocalTransport", () => { baseUrl: "https://lk.example.org", }, ownMembershipIdentity: ownMemberMock, - useMatrix2$: constant(false), + useOldJwtEndpoint$: constant(false), delayId$: constant("delay_id_mock"), }); diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 6c3e1cd0..b44cf967 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -35,7 +35,6 @@ import { } from "../../../livekit/openIDSFU.ts"; import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts"; import { customLivekitUrl } from "../../../settings/settings.ts"; -import { type LivekitTransportWithVersion } from "../remoteMembers/ConnectionManager.ts"; const logger = rootLogger.getChild("[LocalTransport]"); @@ -51,7 +50,7 @@ interface Props { client: Pick & OpenIDClientParts; roomId: string; useOldestMember$: Behavior; - useMatrix2$: Behavior; + useOldJwtEndpoint$: Behavior; delayId$: Behavior; } @@ -62,6 +61,11 @@ interface Props { * @prop useOldestMember Whether to use the same transport as the oldest member. * This will only update once the first oldest member appears. Will not recompute if the oldest member leaves. * + * @prop useOldJwtEndpoint$ Whether to set forceOldJwtEndpoint the use the old JWT endpoint. + * This is used when the connection manager needs to know if it has to use the legacy endpoint which implies a string concatenated rtcBackendIdentity. + * (which is expected for non sticky event based rtc member events) + * @returns Behavior<(LivekitTransport & { forceOldJwtEndpoint: boolean }) | null> The `forceOldJwtEndpoint` field is added to let the connection EncryptionManager + * know that this transport is for the local member and it IS RELEVANT which jwt endpoint to use. (for the local member transport, we need to know which jwt endpoint to use) * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ export const createLocalTransport$ = ({ @@ -71,21 +75,20 @@ export const createLocalTransport$ = ({ client, roomId, useOldestMember$, - useMatrix2$, + useOldJwtEndpoint$, delayId$, -}: Props): Behavior => { +}: Props): Behavior => { /** * The transport over which we should be actively publishing our media. * undefined when not joined. */ const oldestMemberTransport$ = scope.behavior( - memberships$.pipe( - map((memberships) => { + combineLatest([memberships$, useOldJwtEndpoint$]).pipe( + map(([memberships, forceOldJwtEndpoint]) => { const oldestMember = memberships.value[0]; - const t = oldestMember?.getTransport(memberships.value[0]); - if (!t) return null; - // Here we will use the matrix2 information from the oldest member transport. - return { ...t, useMatrix2: oldestMember.kind === "rtc" }; + const transport = oldestMember?.getTransport(memberships.value[0]); + if (!transport) return null; + return { ...transport, forceOldJwtEndpoint }; }), first((t) => t != null && isLivekitTransport(t)), ), @@ -98,24 +101,23 @@ export const createLocalTransport$ = ({ * * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ - const preferredTransport$: Behavior = - scope.behavior( - combineLatest([customLivekitUrl.value$, useMatrix2$, delayId$]).pipe( - switchMap(([customUrl, useMatrix2, delayId]) => - from( - makeTransport( - client, - ownMembershipIdentity, - roomId, - customUrl, - useMatrix2, - delayId ?? undefined, - ), + const preferredTransport$: Behavior = scope.behavior( + combineLatest([customLivekitUrl.value$, delayId$, useOldJwtEndpoint$]).pipe( + switchMap(([customUrl, delayId, forceOldJwtEndpoint]) => + from( + makeTransport( + client, + ownMembershipIdentity, + roomId, + customUrl, + forceOldJwtEndpoint, + delayId ?? undefined, ), ), ), - null, - ); + ), + null, + ); /** * The chosen transport we should advertise in our MatrixRTC membership. @@ -131,7 +133,7 @@ export const createLocalTransport$ = ({ ? (oldestMemberTransport ?? preferredTransport) : preferredTransport, ), - distinctUntilChanged(areLivekitTransportsEqual), + distinctUntilChanged((t1, t2) => areLivekitTransportsEqual(t1, t2)), ), ); }; @@ -142,6 +144,8 @@ const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; * * @param client * @param roomId + * @param useMatrix2 This implies using the matrix2 jwt endpoint (including delayed event delegation of the jwt token) + * @param delayId * @returns * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ @@ -150,9 +154,9 @@ async function makeTransport( membership: CallMembershipIdentityParts, roomId: string, urlFromDevSettings: string | null, - matrix2jwt = false, + forceOldJwtEndpoint: boolean, delayId?: string, -): Promise { +): Promise { let transport: LivekitTransport | undefined; logger.trace("Searching for a preferred transport"); //TODO refactor this to use the jwt service returned alias. @@ -209,11 +213,12 @@ async function makeTransport( client, membership, transport.livekit_service_url, + forceOldJwtEndpoint, transport.livekit_alias, - matrix2jwt, client.baseUrl, delayId, + logger, ); - return { ...transport, useMatrix2: matrix2jwt }; + return { ...transport, forceOldJwtEndpoint }; } diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 533f451a..57578641 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -26,6 +26,7 @@ import fetchMock from "fetch-mock"; import EventEmitter from "events"; import { type IOpenIDToken } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; +import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc/LivekitTransport"; import { Connection, @@ -39,7 +40,6 @@ import { FailToGetOpenIdToken, } from "../../../utils/errors.ts"; import { mockRemoteParticipant, ownMemberMock } from "../../../utils/test.ts"; -import { type LivekitTransportWithVersion } from "./ConnectionManager.ts"; let testScope: ObservableScope; @@ -50,7 +50,7 @@ let fakeLivekitRoom: MockedObject; let localParticipantEventEmiter: EventEmitter; let fakeLocalParticipant: MockedObject; -const livekitFocus: LivekitTransportWithVersion = { +const livekitFocus: LivekitTransport = { livekit_alias: "!roomID:example.org", livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt", type: "livekit", diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index d32bbce6..e070b56b 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -35,8 +35,10 @@ import { } from "../../../utils/errors.ts"; export interface ConnectionOpts { + /** Whether we always try to connect to this connection via the legacy jwt endpoint. (no hash identity) */ + forceOldJwtEndpoint?: boolean; /** The media transport to connect to. */ - transport: LivekitTransport & { useMatrix2: boolean }; + transport: LivekitTransport; /** The Matrix client to use for OpenID and SFU config requests. */ client: OpenIDClientParts; /** The observable scope to use for this connection. */ @@ -89,7 +91,7 @@ export class Connection { /** * The media transport to connect to. */ - public readonly transport: LivekitTransport & { useMatrix2: boolean }; + public readonly transport: LivekitTransport; public readonly livekitRoom: LivekitRoom; @@ -192,16 +194,14 @@ export class Connection { this.client, this.ownMembershipIdentity, this.transport.livekit_service_url, + this.forceOldJwtEndpoint, this.transport.livekit_alias, - this.transport.useMatrix2, + // For the remote members we intentionally do not pass a delayEndpointBaseUrl. + undefined, + // and no delayId. + undefined, + this.logger, ); - // client: OpenIDClientParts, - // membership: CallMembershipIdentityParts, - // serviceUrl: string, - // livekitRoomAlias: string, - // matrix2jwt: boolean, - // delayEndpointBaseUrl?: string, - // delayId?: string, } /** @@ -222,7 +222,7 @@ export class Connection { private readonly client: OpenIDClientParts; private readonly logger: Logger; - + private readonly forceOldJwtEndpoint: boolean; /** * Creates a new connection to a matrix RTC LiveKit backend. * @@ -235,6 +235,7 @@ export class Connection { logger: Logger, private ownMembershipIdentity: CallMembershipIdentityParts, ) { + this.forceOldJwtEndpoint = opts.forceOldJwtEndpoint ?? false; this.logger = logger.getChild("[Connection]"); this.logger.info( `[Connection] Creating new connection to ${opts.transport.livekit_service_url} ${opts.transport.livekit_alias}`, diff --git a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts index 82a1a78a..94652d16 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts @@ -15,6 +15,7 @@ import { import { type Logger } from "matrix-js-sdk/lib/logger"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; +import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc/LivekitTransport"; import { type ObservableScope } from "../../ObservableScope.ts"; import { Connection } from "./Connection.ts"; @@ -23,15 +24,15 @@ import type { MediaDevices } from "../../MediaDevices.ts"; import type { Behavior } from "../../Behavior.ts"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import { defaultLiveKitOptions } from "../../../livekit/options.ts"; -import { type LivekitTransportWithVersion } from "./ConnectionManager.ts"; // TODO evaluate if this should be done like the Publisher Factory export interface ConnectionFactory { createConnection( - transport: LivekitTransportWithVersion, + transport: LivekitTransport, scope: ObservableScope, logger: Logger, ownMembershipIdentity: CallMembershipIdentityParts, + forceOldJwtEndpoint?: boolean, ): Connection; } @@ -88,10 +89,11 @@ export class ECConnectionFactory implements ConnectionFactory { * @returns */ public createConnection( - transport: LivekitTransportWithVersion, + transport: LivekitTransport, scope: ObservableScope, logger: Logger, ownMembershipIdentity: CallMembershipIdentityParts, + forceOldJwtEndpoint?: boolean, ): Connection { return new Connection( { @@ -99,6 +101,7 @@ export class ECConnectionFactory implements ConnectionFactory { client: this.client, scope: scope, livekitRoomFactory: this.livekitRoomFactory, + forceOldJwtEndpoint, }, logger, ownMembershipIdentity, diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts index 4ab91646..088bf41b 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts @@ -14,29 +14,26 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { Epoch, mapEpoch, ObservableScope } from "../../ObservableScope.ts"; import { createConnectionManager$, - type LivekitTransportWithVersion, type ConnectionManagerData, } from "./ConnectionManager.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type Connection } from "./Connection.ts"; import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts"; import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; -import { type Behavior } from "../../Behavior.ts"; +import { constant, type Behavior } from "../../Behavior.ts"; // Some test constants -const TRANSPORT_1: LivekitTransportWithVersion = { +const TRANSPORT_1: LivekitTransport = { type: "livekit", livekit_service_url: "https://lk.example.org", livekit_alias: "!alias:example.org", - useMatrix2: false, }; -const TRANSPORT_2: LivekitTransportWithVersion = { +const TRANSPORT_2: LivekitTransport = { type: "livekit", livekit_service_url: "https://lk.sample.com", livekit_alias: "!alias:sample.com", - useMatrix2: false, }; let fakeConnectionFactory: ConnectionFactory; @@ -79,7 +76,8 @@ describe("connections$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - inputTransports$: behavior("a", { + localTransport$: constant(null), + remoteTransports$: behavior("a", { a: new Epoch([TRANSPORT_1, TRANSPORT_2], 0), }), logger: logger, @@ -119,7 +117,8 @@ describe("connections$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - inputTransports$: behavior("abcdef", { + localTransport$: constant(null), + remoteTransports$: behavior("abcdef", { a: new Epoch([TRANSPORT_1], 0), b: new Epoch([TRANSPORT_1], 1), c: new Epoch([TRANSPORT_1], 2), @@ -165,7 +164,8 @@ describe("connections$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - inputTransports$: behavior("abc", { + localTransport$: constant(null), + remoteTransports$: behavior("abc", { a: new Epoch([TRANSPORT_1], 0), b: new Epoch([TRANSPORT_1, TRANSPORT_2], 1), c: new Epoch([TRANSPORT_1], 2), @@ -281,7 +281,8 @@ describe("connectionManagerData$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - inputTransports$: behavior("a", { + localTransport$: constant(null), + remoteTransports$: behavior("a", { a: new Epoch([TRANSPORT_1, TRANSPORT_2], 0), }), logger, diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index d5852d84..6101f79b 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -7,22 +7,18 @@ Please see LICENSE in the repository root for full details. */ import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; -import { combineLatest, map, of, switchMap, tap } from "rxjs"; +import { combineLatest, map, of, switchMap } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type RemoteParticipant } from "livekit-client"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; -import { type Behavior } from "../../Behavior.ts"; +import { constant, type Behavior } from "../../Behavior.ts"; import { type Connection } from "./Connection.ts"; import { Epoch, type ObservableScope } from "../../ObservableScope.ts"; import { generateItemsWithEpoch } from "../../../utils/observable.ts"; import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts"; -export type LivekitTransportWithVersion = LivekitTransport & { - useMatrix2: boolean; -}; - export class ConnectionManagerData { private readonly store: Map = new Map(); @@ -64,7 +60,9 @@ export class ConnectionManagerData { interface Props { scope: ObservableScope; connectionFactory: ConnectionFactory; - inputTransports$: Behavior>; + localTransport$: Behavior; + remoteTransports$: Behavior>; + forceOldJwtEndpointForLocalTransport$?: Behavior; logger: Logger; ownMembershipIdentity: CallMembershipIdentityParts; } @@ -91,13 +89,29 @@ export interface IConnectionManager { export function createConnectionManager$({ scope, connectionFactory, - inputTransports$, + localTransport$, + remoteTransports$, + forceOldJwtEndpointForLocalTransport$ = constant(false), logger: parentLogger, ownMembershipIdentity, }: Props): IConnectionManager { const logger = parentLogger.getChild("[ConnectionManager]"); // TODO logger: only construct one logger from the client and make it compatible via a EC specific sing + const allInputTransports$ = combineLatest([ + localTransport$, + remoteTransports$, + ]).pipe( + map(([localTransport, transports]) => { + const localTransportAsArray = localTransport ? [localTransport] : []; + return transports.mapInner((transports) => [ + ...localTransportAsArray, + ...transports, + ]); + }), + map((transports) => transports.mapInner(removeDuplicateTransports)), + ); + /** * All transports currently managed by the ConnectionManager. * @@ -106,14 +120,32 @@ export function createConnectionManager$({ * It is build based on the list of subscribed transports (`transportsSubscriptions$`). * externally this is modified via `registerTransports()`. */ - const transports$ = scope.behavior( - inputTransports$.pipe( - map((transports) => transports.mapInner(removeDuplicateTransports)), - tap(({ value: transports }) => { - logger.trace( - `Managing transports: ${transports.map((t) => t.livekit_service_url).join(", ")}`, - ); - }), + const transportsWithJwtTag$ = scope.behavior( + combineLatest([ + allInputTransports$, + localTransport$, + forceOldJwtEndpointForLocalTransport$, + ]).pipe( + map( + ([ + transports, + localTransport, + forceOldJwtEndpointForLocalTransport, + ]) => { + // nmodify only the local transport with forceOldJwtEndpointForLocalTransport + const index = transports.value.findIndex((t) => + areLivekitTransportsEqual(localTransport, t), + ); + transports.value[index].forceOldJwtEndpoint = + forceOldJwtEndpointForLocalTransport; + logger.trace( + `Managing transports: ${transports.value.map((t) => t.livekit_service_url).join(", ")}`, + ); + return transports as Epoch< + (LivekitTransport & { forceOldJwtEndpoint?: boolean })[] + >; + }, + ), ), ); @@ -121,7 +153,7 @@ export function createConnectionManager$({ * Connections for each transport in use by one or more session members. */ const connections$ = scope.behavior( - transports$.pipe( + transportsWithJwtTag$.pipe( generateItemsWithEpoch( function* (transports) { for (const transport of transports) @@ -129,23 +161,23 @@ export function createConnectionManager$({ keys: [ transport.livekit_service_url, transport.livekit_alias, - transport.useMatrix2, + transport.forceOldJwtEndpoint, ], data: undefined, }; }, - (scope, _data$, serviceUrl, alias, useMatrix2) => { + (scope, _data$, serviceUrl, alias, forceOldJwtEndpoint) => { logger.debug(`Creating connection to ${serviceUrl} (${alias})`); const connection = connectionFactory.createConnection( { type: "livekit", livekit_service_url: serviceUrl, livekit_alias: alias, - useMatrix2, }, scope, logger, ownMembershipIdentity, + forceOldJwtEndpoint, ); // Start the connection immediately // Use connection state to track connection progress diff --git a/src/state/CallViewModel/remoteMembers/integration.test.ts b/src/state/CallViewModel/remoteMembers/integration.test.ts index 00062c60..df10c861 100644 --- a/src/state/CallViewModel/remoteMembers/integration.test.ts +++ b/src/state/CallViewModel/remoteMembers/integration.test.ts @@ -13,11 +13,7 @@ import fetchMock from "fetch-mock"; import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { logger } from "matrix-js-sdk/lib/logger"; -import { - type Epoch, - ObservableScope, - trackEpoch, -} from "../../ObservableScope.ts"; +import { type Epoch, ObservableScope, trackEpoch } from "../../ObservableScope.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import { @@ -34,6 +30,7 @@ import { } from "./MatrixLivekitMembers.ts"; import { createConnectionManager$ } from "./ConnectionManager.ts"; import { membershipsAndTransports$ } from "../../SessionBehaviors.ts"; +import { constant } from "../../Behavior.ts"; // Test the integration of ConnectionManager and MatrixLivekitMerger @@ -121,7 +118,8 @@ test("bob, carl, then bob joining no tracks yet", () => { const connectionManager = createConnectionManager$({ scope: testScope, connectionFactory: ecConnectionFactory, - inputTransports$: membershipsAndTransports.transports$, + localTransport$: constant(null), + remoteTransports$: membershipsAndTransports.transports$, logger: logger, ownMembershipIdentity: ownMemberMock, }); diff --git a/src/state/SessionBehaviors.ts b/src/state/SessionBehaviors.ts index b61d2fe6..e174a1cc 100644 --- a/src/state/SessionBehaviors.ts +++ b/src/state/SessionBehaviors.ts @@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details. import { type CallMembership, isLivekitTransport, + type LivekitTransport, type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; @@ -20,18 +21,15 @@ import { type ObservableScope, } from "./ObservableScope"; import { type Behavior } from "./Behavior"; -import { type LivekitTransportWithVersion } from "./CallViewModel/remoteMembers/ConnectionManager"; export const membershipsAndTransports$ = ( scope: ObservableScope, memberships$: Behavior>, ): { membershipsWithTransport$: Behavior< - Epoch< - { membership: CallMembership; transport?: LivekitTransportWithVersion }[] - > + Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> >; - transports$: Behavior>; + transports$: Behavior>; } => { /** * Lists the transports used by ourselves, plus all other MatrixRTC session @@ -49,12 +47,7 @@ export const membershipsAndTransports$ = ( const transport = membership.getTransport(oldestMembership); return { membership, - transport: isLivekitTransport(transport) - ? { - ...transport, - useMatrix2: membership.kind === "rtc", - } - : undefined, + transport: isLivekitTransport(transport) ? transport : undefined, }; }); }), diff --git a/src/utils/test.ts b/src/utils/test.ts index a860bde0..02277af0 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -25,6 +25,7 @@ import { import { CallMembership, type LivekitFocusSelection, + type LivekitTransport, type MatrixRTCSession, MatrixRTCSessionEvent, type MatrixRTCSessionEventHandlerMap, @@ -66,7 +67,6 @@ import { type MediaDevices } from "../state/MediaDevices"; import { type Behavior, constant } from "../state/Behavior"; import { ObservableScope } from "../state/ObservableScope"; import { MuteStates } from "../state/MuteStates"; -import { type LivekitTransportWithVersion } from "../state/CallViewModel/remoteMembers/ConnectionManager"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); @@ -197,11 +197,10 @@ export function mockEmitter(): EmitterMock { }; } -export const exampleTransport: LivekitTransportWithVersion = { +export const exampleTransport: LivekitTransport = { type: "livekit", livekit_service_url: "https://lk.example.org", livekit_alias: "!alias:example.org", - useMatrix2: false, }; export function mockCallMembership( diff --git a/yarn.lock b/yarn.lock index 0ddaad61..83555527 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7533,7 +7533,7 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4" + matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=6f0815930a008eff8f86e6e5748d447be0e7c25e" matrix-widget-api: "npm:^1.14.0" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -10338,9 +10338,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=6f0815930a008eff8f86e6e5748d447be0e7c25e": version: 39.4.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=8cea2c05839ebcaa90945504a453b9b1e1092fc4" + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=6f0815930a008eff8f86e6e5748d447be0e7c25e" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" @@ -10356,7 +10356,7 @@ __metadata: sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/2375dd3d9191f78fe589b0d3170f3da7792ed469a81d3ba3cd12f4915fd33a859f8af3491edb9cf0cdaa1f881a3ea7c1bf7539e850ad0360ec9981271f462c81 + checksum: 10c0/a5a904a79f3660d1f6fe217195e662adf82af4a445681e47f292772d9d4d63ce60aaca209f40c41e2d659bee2b17cd5b3345bbad77795032057f2c0e3129cc77 languageName: node linkType: hard From 057bc4e2ad03f9b54fcfe48b222368e3767e6b4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:06:04 +0000 Subject: [PATCH 13/76] Update all non-major dependencies --- package.json | 6 +- yarn.lock | 2186 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 1418 insertions(+), 774 deletions(-) diff --git a/package.json b/package.json index c67c2e4c..263682b2 100644 --- a/package.json +++ b/package.json @@ -42,16 +42,16 @@ "@codecov/vite-plugin": "^1.3.0", "@fontsource/inconsolata": "^5.1.0", "@fontsource/inter": "^5.1.0", - "@formatjs/intl-durationformat": "^0.7.0", + "@formatjs/intl-durationformat": "^0.9.0", "@formatjs/intl-segmenter": "^11.7.3", "@livekit/components-core": "^0.12.0", "@livekit/components-react": "^2.0.0", "@livekit/protocol": "^1.42.2", - "@livekit/track-processors": "^0.5.5", + "@livekit/track-processors": "^0.7.0", "@mediapipe/tasks-vision": "^0.10.18", "@opentelemetry/api": "^1.4.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index abb3ef95..04dc117b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -60,7 +60,7 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": +"@ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" dependencies: @@ -122,33 +122,40 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.0": +"@babel/compat-data@npm:^7.27.7": version: 7.28.0 resolution: "@babel/compat-data@npm:7.28.0" checksum: 10c0/c4e527302bcd61052423f757355a71c3bc62362bac13f7f130de16e439716f66091ff5bdecda418e8fa0271d4c725f860f0ee23ab7bf6e769f7a8bb16dfcb531 languageName: node linkType: hard +"@babel/compat-data@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/compat-data@npm:7.28.5" + checksum: 10c0/702a25de73087b0eba325c1d10979eed7c9b6662677386ba7b5aa6eace0fc0676f78343bae080a0176ae26f58bd5535d73b9d0fbb547fef377692e8b249353a7 + languageName: node + linkType: hard + "@babel/core@npm:^7.16.5, @babel/core@npm:^7.18.5, @babel/core@npm:^7.21.3, @babel/core@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/core@npm:7.28.0" + version: 7.28.5 + resolution: "@babel/core@npm:7.28.5" dependencies: - "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.5" "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-module-transforms": "npm:^7.27.3" - "@babel/helpers": "npm:^7.27.6" - "@babel/parser": "npm:^7.28.0" + "@babel/helper-module-transforms": "npm:^7.28.3" + "@babel/helpers": "npm:^7.28.4" + "@babel/parser": "npm:^7.28.5" "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/423302e7c721e73b1c096217880272e02020dfb697a55ccca60ad01bba90037015f84d0c20c6ce297cf33a19bb704bc5c2b3d3095f5284dfa592bd1de0b9e8c3 + checksum: 10c0/535f82238027621da6bdffbdbe896ebad3558b311d6f8abc680637a9859b96edbf929ab010757055381570b29cf66c4a295b5618318d27a4273c0e2033925e72 languageName: node linkType: hard @@ -191,6 +198,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/generator@npm:7.28.5" + dependencies: + "@babel/parser": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-annotate-as-pure@npm:7.25.9" @@ -239,6 +259,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.28.3, @babel/helper-create-class-features-plugin@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.5" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" + "@babel/helper-optimise-call-expression": "npm:^7.27.1" + "@babel/helper-replace-supers": "npm:^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.5" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/786a6514efcf4514aaad85beed419b9184d059f4c9a9a95108f320142764999827252a851f7071de19f29424d369616573ecbaa347f1ce23fb12fc6827d9ff56 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6": version: 7.26.3 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.26.3" @@ -297,6 +334,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" + dependencies: + "@babel/traverse": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + checksum: 10c0/4e6e05fbf4dffd0bc3e55e28fcaab008850be6de5a7013994ce874ec2beb90619cda4744b11607a60f8aae0227694502908add6188ceb1b5223596e765b44814 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-module-imports@npm:7.27.1" @@ -307,7 +354,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.27.3": +"@babel/helper-module-transforms@npm:^7.27.1": version: 7.27.3 resolution: "@babel/helper-module-transforms@npm:7.27.3" dependencies: @@ -320,6 +367,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helper-module-transforms@npm:7.28.3" + dependencies: + "@babel/helper-module-imports": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.3" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-optimise-call-expression@npm:7.27.1" @@ -407,6 +467,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-validator-identifier@npm:7.28.5" + checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-option@npm:7.27.1" @@ -425,13 +492,13 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.6": - version: 7.27.6 - resolution: "@babel/helpers@npm:7.27.6" +"@babel/helpers@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/helpers@npm:7.28.4" dependencies: "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.27.6" - checksum: 10c0/448bac96ef8b0f21f2294a826df9de6bf4026fd023f8a6bb6c782fe3e61946801ca24381490b8e58d861fee75cd695a1882921afbf1f53b0275ee68c938bd6d3 + "@babel/types": "npm:^7.28.4" + checksum: 10c0/aaa5fb8098926dfed5f223adf2c5e4c7fbba4b911b73dfec2d7d3083f8ba694d201a206db673da2d9b3ae8c01793e795767654558c450c8c14b4c2175b4fcb44 languageName: node linkType: hard @@ -502,15 +569,26 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.27.1" +"@babel/parser@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/parser@npm:7.28.5" + dependencies: + "@babel/types": "npm:^7.28.5" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/5bbe48bf2c79594ac02b490a41ffde7ef5aa22a9a88ad6bcc78432a6ba8a9d638d531d868bd1f104633f1f6bba9905746e15185b8276a3756c42b765d131b1ef + languageName: node + linkType: hard + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/7dfffa978ae1cd179641a7c4b4ad688c6828c2c58ec96b118c2fb10bc3715223de6b88bff1ebff67056bb5fccc568ae773e3b83c592a1b843423319f80c99ebd + checksum: 10c0/844b7c7e9eec6d858262b2f3d5af75d3a6bbd9d3ecc740d95271fbdd84985731674536f5d8ac98f2dc0e8872698b516e406636e4d0cb04b50afe471172095a53 languageName: node linkType: hard @@ -549,15 +627,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.27.1" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.28.3" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.3" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/b94e6c3fc019e988b1499490829c327a1067b4ddea8ad402f6d0554793c9124148c2125338c723661b6dff040951abc1f092afbf3f2d234319cd580b68e52445 + checksum: 10c0/3cdc27c4e08a632a58e62c6017369401976edf1cd9ae73fd9f0d6770ddd9accf40b494db15b66bab8db2a8d5dc5bab5ca8c65b19b81fdca955cd8cbbe24daadb languageName: node linkType: hard @@ -674,14 +752,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-block-scoping@npm:7.28.0" +"@babel/plugin-transform-block-scoping@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-block-scoping@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/787d85e72a92917e735aa54e23062fa777031f8a07046e67f5026eff3d91e64eb535575dd1df917b0011bee014ae51287478af14c1d4ba60bc81e326bc044cfc + checksum: 10c0/6b098887b375c23813ccee7a00179501fc5f709b4ee5a4b2a5c5c9ef3b44cee49e240214b1a9b4ad2bd1911fab3335eac2f0a3c5f014938a1b61bec84cec4845 languageName: node linkType: hard @@ -697,31 +775,31 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-class-static-block@npm:7.27.1" +"@babel/plugin-transform-class-static-block@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/plugin-transform-class-static-block@npm:7.28.3" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.3" "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10c0/396997dd81fc1cf242b921e337d25089d6b9dc3596e81322ff11a6359326dc44f2f8b82dcc279c2e514cafaf8964dc7ed39e9fab4b8af1308b57387d111f6a20 + checksum: 10c0/8c922a64f6f5b359f7515c89ef0037bad583b4484dfebc1f6bc1cf13462547aaceb19788827c57ec9a2d62495f34c4b471ca636bf61af00fdaea5e9642c82b60 languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-classes@npm:7.28.0" +"@babel/plugin-transform-classes@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/plugin-transform-classes@npm:7.28.4" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.27.3" "@babel/helper-compilation-targets": "npm:^7.27.2" "@babel/helper-globals": "npm:^7.28.0" "@babel/helper-plugin-utils": "npm:^7.27.1" "@babel/helper-replace-supers": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/3b213b43104fe99dd7e79401a86d09e545836e057a70ffe77e8196a87bf67ae167e502ae90afdf0d1a2be683be5652514aaeda743bd984e583523dd8ecfef887 + checksum: 10c0/76687ed37216ff012c599870dc00183fb716f22e1a02fe9481943664c0e4d0d88c3da347dc3fe290d4728f4d47cd594ffa621d23845e2bb8ab446e586308e066 languageName: node linkType: hard @@ -749,6 +827,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-destructuring@npm:7.28.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/288207f488412b23bb206c7c01ba143714e2506b72a9ec09e993f28366cc8188d121bde714659b3437984a86d2881d9b1b06de3089d5582823ccf2f3b3eaa2c4 + languageName: node + linkType: hard + "@babel/plugin-transform-dotall-regex@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-dotall-regex@npm:7.27.1" @@ -807,14 +897,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.27.1" +"@babel/plugin-transform-exponentiation-operator@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/953d21e01fed76da8e08fb5094cade7bf8927c1bb79301916bec2db0593b41dbcfbca1024ad5db886b72208a93ada8f57a219525aad048cf15814eeb65cf760d + checksum: 10c0/006566e003c2a8175346cc4b3260fcd9f719b912ceae8a4e930ce02ee3cf0b2841d5c21795ba71790871783d3c0c1c3d22ce441b8819c37975844bfba027d3f7 languageName: node linkType: hard @@ -876,14 +966,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.27.1" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/5b0abc7c0d09d562bf555c646dce63a30288e5db46fd2ce809a61d064415da6efc3b2b3c59b8e4fe98accd072c89a2f7c3765b400e4bf488651735d314d9feeb + checksum: 10c0/fba4faa96d86fa745b0539bb631deee3f2296f0643c087a50ad0fac2e5f0a787fa885e9bdd90ae3e7832803f3c08e7cd3f1e830e7079dbdc023704923589bb23 languageName: node linkType: hard @@ -922,17 +1012,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.27.1" +"@babel/plugin-transform-modules-systemjs@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5" dependencies: - "@babel/helper-module-transforms": "npm:^7.27.1" + "@babel/helper-module-transforms": "npm:^7.28.3" "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@babel/traverse": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/f16fca62d144d9cbf558e7b5f83e13bb6d0f21fdeff3024b0cecd42ffdec0b4151461da42bd0963512783ece31aafa5ffe03446b4869220ddd095b24d414e2b5 + checksum: 10c0/7e8c0bcff79689702b974f6a0fedb5d0c6eeb5a5e3384deb7028e7cfe92a5242cc80e981e9c1817aad29f2ecc01841753365dd38d877aa0b91737ceec2acfd07 languageName: node linkType: hard @@ -993,18 +1083,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.0" +"@babel/plugin-transform-object-rest-spread@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.4" dependencies: "@babel/helper-compilation-targets": "npm:^7.27.2" "@babel/helper-plugin-utils": "npm:^7.27.1" "@babel/plugin-transform-destructuring": "npm:^7.28.0" "@babel/plugin-transform-parameters": "npm:^7.27.7" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/360dc6fd5285ee5e1d3be8a1fb0decd120b2a1726800317b4ab48b7c91616247030239b7fa06ceaa1a8a586fde1e143c24d45f8d41956876099d97d664f8ef1e + checksum: 10c0/81725c8d6349957899975f3f789b1d4fb050ee8b04468ebfaccd5b59e0bda15cbfdef09aee8b4359f322b6715149d680361f11c1a420c4bdbac095537ecf7a90 languageName: node linkType: hard @@ -1043,6 +1133,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-chaining@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/adf5f70b1f9eb0dd6ff3d159a714683af3c910775653e667bd9f864c3dc2dc9872aba95f6c1e5f2a9675067241942f4fd0d641147ef4bf2bd8bc15f1fa0f2ed5 + languageName: node + linkType: hard + "@babel/plugin-transform-parameters@npm:^7.27.7": version: 7.27.7 resolution: "@babel/plugin-transform-parameters@npm:7.27.7" @@ -1090,14 +1192,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-display-name@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-react-display-name@npm:7.27.1" +"@babel/plugin-transform-react-display-name@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/plugin-transform-react-display-name@npm:7.28.0" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/6cd474b5fb30a2255027d8fc19975aee1c1da54dd8bc8b79802676096182ca4136302ce65a24fbb277f8fe30f266006bbf327ef6be2846d3681eb57509744125 + checksum: 10c0/f5f86d2ad92be3e962158f344c2e385e23e2dfae7c8c7dc32138fb2cc46f63f5e50386c9f6c6fc16dbf1792c7bb650ad92c18203d0c2c0bd875bc28b0b80ef30 languageName: node linkType: hard @@ -1161,14 +1263,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.28.0": - version: 7.28.1 - resolution: "@babel/plugin-transform-regenerator@npm:7.28.1" +"@babel/plugin-transform-regenerator@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/plugin-transform-regenerator@npm:7.28.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/6c9e6eb80ce9c0bde0876c80979e078fbc85dc802272cba4ee72b5b1c858472e38167c418917e4f0d4384ce888706d95544a8d266880c0e199e167e078168b67 + checksum: 10c0/5ad14647ffaac63c920e28df1b580ee2e932586bbdc71f61ec264398f68a5406c71a7f921de397a41b954a69316c5ab90e5d789ffa2bb34c5e6feb3727cfefb8 languageName: node linkType: hard @@ -1251,18 +1353,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-typescript@npm:7.27.1" +"@babel/plugin-transform-typescript@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-typescript@npm:7.28.5" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-create-class-features-plugin": "npm:^7.28.5" "@babel/helper-plugin-utils": "npm:^7.27.1" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" "@babel/plugin-syntax-typescript": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/48f1db5de17a0f9fc365ff4fb046010aedc7aad813a7aa42fb73fcdab6442f9e700dde2cc0481086e01b0dae662ae4d3e965a52cde154f0f146d243a8ac68e93 + checksum: 10c0/09e574ba5462e56452b4ceecae65e53c8e697a2d3559ce5d210bed10ac28a18aa69377e7550c30520eb29b40c417ee61997d5d58112657f22983244b78915a7c languageName: node linkType: hard @@ -1314,18 +1416,18 @@ __metadata: linkType: hard "@babel/preset-env@npm:^7.22.20": - version: 7.28.0 - resolution: "@babel/preset-env@npm:7.28.0" + version: 7.28.5 + resolution: "@babel/preset-env@npm:7.28.5" dependencies: - "@babel/compat-data": "npm:^7.28.0" + "@babel/compat-data": "npm:^7.28.5" "@babel/helper-compilation-targets": "npm:^7.27.2" "@babel/helper-plugin-utils": "npm:^7.27.1" "@babel/helper-validator-option": "npm:^7.27.1" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope": "npm:^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.27.1" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.28.3" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-import-assertions": "npm:^7.27.1" "@babel/plugin-syntax-import-attributes": "npm:^7.27.1" @@ -1334,42 +1436,42 @@ __metadata: "@babel/plugin-transform-async-generator-functions": "npm:^7.28.0" "@babel/plugin-transform-async-to-generator": "npm:^7.27.1" "@babel/plugin-transform-block-scoped-functions": "npm:^7.27.1" - "@babel/plugin-transform-block-scoping": "npm:^7.28.0" + "@babel/plugin-transform-block-scoping": "npm:^7.28.5" "@babel/plugin-transform-class-properties": "npm:^7.27.1" - "@babel/plugin-transform-class-static-block": "npm:^7.27.1" - "@babel/plugin-transform-classes": "npm:^7.28.0" + "@babel/plugin-transform-class-static-block": "npm:^7.28.3" + "@babel/plugin-transform-classes": "npm:^7.28.4" "@babel/plugin-transform-computed-properties": "npm:^7.27.1" - "@babel/plugin-transform-destructuring": "npm:^7.28.0" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" "@babel/plugin-transform-dotall-regex": "npm:^7.27.1" "@babel/plugin-transform-duplicate-keys": "npm:^7.27.1" "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.27.1" "@babel/plugin-transform-dynamic-import": "npm:^7.27.1" "@babel/plugin-transform-explicit-resource-management": "npm:^7.28.0" - "@babel/plugin-transform-exponentiation-operator": "npm:^7.27.1" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.28.5" "@babel/plugin-transform-export-namespace-from": "npm:^7.27.1" "@babel/plugin-transform-for-of": "npm:^7.27.1" "@babel/plugin-transform-function-name": "npm:^7.27.1" "@babel/plugin-transform-json-strings": "npm:^7.27.1" "@babel/plugin-transform-literals": "npm:^7.27.1" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.27.1" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.28.5" "@babel/plugin-transform-member-expression-literals": "npm:^7.27.1" "@babel/plugin-transform-modules-amd": "npm:^7.27.1" "@babel/plugin-transform-modules-commonjs": "npm:^7.27.1" - "@babel/plugin-transform-modules-systemjs": "npm:^7.27.1" + "@babel/plugin-transform-modules-systemjs": "npm:^7.28.5" "@babel/plugin-transform-modules-umd": "npm:^7.27.1" "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.27.1" "@babel/plugin-transform-new-target": "npm:^7.27.1" "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.27.1" "@babel/plugin-transform-numeric-separator": "npm:^7.27.1" - "@babel/plugin-transform-object-rest-spread": "npm:^7.28.0" + "@babel/plugin-transform-object-rest-spread": "npm:^7.28.4" "@babel/plugin-transform-object-super": "npm:^7.27.1" "@babel/plugin-transform-optional-catch-binding": "npm:^7.27.1" - "@babel/plugin-transform-optional-chaining": "npm:^7.27.1" + "@babel/plugin-transform-optional-chaining": "npm:^7.28.5" "@babel/plugin-transform-parameters": "npm:^7.27.7" "@babel/plugin-transform-private-methods": "npm:^7.27.1" "@babel/plugin-transform-private-property-in-object": "npm:^7.27.1" "@babel/plugin-transform-property-literals": "npm:^7.27.1" - "@babel/plugin-transform-regenerator": "npm:^7.28.0" + "@babel/plugin-transform-regenerator": "npm:^7.28.4" "@babel/plugin-transform-regexp-modifiers": "npm:^7.27.1" "@babel/plugin-transform-reserved-words": "npm:^7.27.1" "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1" @@ -1389,7 +1491,7 @@ __metadata: semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/f343103b8f0e8da5be4ae031aff8bf35da4764997af4af78ae9506f421b785dd45da1bc09f845b1fc308c8b7d134aead4a1f89e7fb6e213cd2f9fe1d2aa78bc9 + checksum: 10c0/d1b730158de290f1c54ed7db0f4fed3f82db5f868ab0a4cb3fc2ea76ed683b986ae136f6e7eb0b44b91bc9a99039a2559851656b4fd50193af1a815a3e32e524 languageName: node linkType: hard @@ -1407,33 +1509,33 @@ __metadata: linkType: hard "@babel/preset-react@npm:^7.22.15": - version: 7.27.1 - resolution: "@babel/preset-react@npm:7.27.1" + version: 7.28.5 + resolution: "@babel/preset-react@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" "@babel/helper-validator-option": "npm:^7.27.1" - "@babel/plugin-transform-react-display-name": "npm:^7.27.1" + "@babel/plugin-transform-react-display-name": "npm:^7.28.0" "@babel/plugin-transform-react-jsx": "npm:^7.27.1" "@babel/plugin-transform-react-jsx-development": "npm:^7.27.1" "@babel/plugin-transform-react-pure-annotations": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/a80b02ef08b026cb9830d6512d08c7cd378eef4c0631dacba4aa1106240d9bb76af6373463f0255f4bbdbfcce40375a61e92735375906ba5871629b0c314bc45 + checksum: 10c0/0d785e708ff301f4102bd4738b77e550e32f981e54dfd3de1191b4d68306bbb934d2d465fc78a6bc22fff0a6b3ce3195a53984f52755c4349e7264c7e01e8c7c languageName: node linkType: hard "@babel/preset-typescript@npm:^7.23.0": - version: 7.27.1 - resolution: "@babel/preset-typescript@npm:7.27.1" + version: 7.28.5 + resolution: "@babel/preset-typescript@npm:7.28.5" dependencies: "@babel/helper-plugin-utils": "npm:^7.27.1" "@babel/helper-validator-option": "npm:^7.27.1" "@babel/plugin-syntax-jsx": "npm:^7.27.1" "@babel/plugin-transform-modules-commonjs": "npm:^7.27.1" - "@babel/plugin-transform-typescript": "npm:^7.27.1" + "@babel/plugin-transform-typescript": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/cba6ca793d915f8aff9fe2f13b0dfbf5fd3f2e9a17f17478ec9878e9af0d206dcfe93154b9fd353727f16c1dca7c7a3ceb4943f8d28b216235f106bc0fbbcaa3 + checksum: 10c0/b3d55548854c105085dd80f638147aa8295bc186d70492289242d6c857cb03a6c61ec15186440ea10ed4a71cdde7d495f5eb3feda46273f36b0ac926e8409629 languageName: node linkType: hard @@ -1545,6 +1647,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/traverse@npm:7.28.5" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.28.5" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.5" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.28.5" + debug: "npm:^4.3.1" + checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.10.3, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3": version: 7.26.0 resolution: "@babel/types@npm:7.26.0" @@ -1585,16 +1702,6 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.27.6": - version: 7.27.7 - resolution: "@babel/types@npm:7.27.7" - dependencies: - "@babel/helper-string-parser": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10c0/1d1dcb5fa7cfba2b4034a3ab99ba17049bfc4af9e170935575246cdb1cee68b04329a0111506d9ae83fb917c47dbd4394a6db5e32fbd041b7834ffbb17ca086b - languageName: node - linkType: hard - "@babel/types@npm:^7.28.0": version: 7.28.2 resolution: "@babel/types@npm:7.28.2" @@ -1605,6 +1712,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/types@npm:7.28.5" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10c0/a5a483d2100befbf125793640dec26b90b95fd233a94c19573325898a5ce1e52cdfa96e495c7dcc31b5eca5b66ce3e6d4a0f5a4a62daec271455959f208ab08a + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^1.0.2": version: 1.0.2 resolution: "@bcoe/v8-coverage@npm:1.0.2" @@ -1662,10 +1779,10 @@ __metadata: languageName: node linkType: hard -"@csstools/color-helpers@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/color-helpers@npm:5.0.2" - checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b +"@csstools/color-helpers@npm:^5.1.0": + version: 5.1.0 + resolution: "@csstools/color-helpers@npm:5.1.0" + checksum: 10c0/b7f99d2e455cf1c9b41a67a5327d5d02888cd5c8802a68b1887dffef537d9d4bc66b3c10c1e62b40bbed638b6c1d60b85a232f904ed7b39809c4029cb36567db languageName: node linkType: hard @@ -1689,19 +1806,6 @@ __metadata: languageName: node linkType: hard -"@csstools/css-color-parser@npm:^3.0.10": - version: 3.0.10 - resolution: "@csstools/css-color-parser@npm:3.0.10" - dependencies: - "@csstools/color-helpers": "npm:^5.0.2" - "@csstools/css-calc": "npm:^2.1.4" - peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.5 - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10c0/8f8a2395b117c2f09366b5c9bf49bc740c92a65b6330fe3cc1e76abafd0d1000e42a657d7b0a3814846a66f1d69896142f7e36d7a4aca77de977e5cc5f944747 - languageName: node - linkType: hard - "@csstools/css-color-parser@npm:^3.0.7": version: 3.0.7 resolution: "@csstools/css-color-parser@npm:3.0.7" @@ -1715,6 +1819,19 @@ __metadata: languageName: node linkType: hard +"@csstools/css-color-parser@npm:^3.1.0": + version: 3.1.0 + resolution: "@csstools/css-color-parser@npm:3.1.0" + dependencies: + "@csstools/color-helpers": "npm:^5.1.0" + "@csstools/css-calc": "npm:^2.1.4" + peerDependencies: + "@csstools/css-parser-algorithms": ^3.0.5 + "@csstools/css-tokenizer": ^3.0.4 + checksum: 10c0/0e0c670ad54ec8ec4d9b07568b80defd83b9482191f5e8ca84ab546b7be6db5d7cc2ba7ac9fae54488b129a4be235d6183d3aab4416fec5e89351f73af4222c5 + languageName: node + linkType: hard + "@csstools/css-parser-algorithms@npm:^3.0.4": version: 3.0.4 resolution: "@csstools/css-parser-algorithms@npm:3.0.4" @@ -1757,6 +1874,21 @@ __metadata: languageName: node linkType: hard +"@csstools/postcss-alpha-function@npm:^1.0.1": + version: 1.0.1 + resolution: "@csstools/postcss-alpha-function@npm:1.0.1" + dependencies: + "@csstools/css-color-parser": "npm:^3.1.0" + "@csstools/css-parser-algorithms": "npm:^3.0.5" + "@csstools/css-tokenizer": "npm:^3.0.4" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" + "@csstools/utilities": "npm:^2.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/35ca209e572534ade21ac5c18aad702aa492eb39e2d0e475f441371063418fe9650554e6a59b1318d3a615da83ef54d9a588faa27063ecc0a568ef7290a6b488 + languageName: node + linkType: hard + "@csstools/postcss-cascade-layers@npm:^5.0.2": version: 5.0.2 resolution: "@csstools/postcss-cascade-layers@npm:5.0.2" @@ -1769,62 +1901,92 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-color-function@npm:^4.0.10": - version: 4.0.10 - resolution: "@csstools/postcss-color-function@npm:4.0.10" +"@csstools/postcss-color-function-display-p3-linear@npm:^1.0.1": + version: 1.0.1 + resolution: "@csstools/postcss-color-function-display-p3-linear@npm:1.0.1" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/a6e65d37a114f95634a07660daa1aa52f4abfb6ddd740cc9267967a5948f5c72469a6ba2432ab1f31616d6f1a4ab963b69f778497496986535831b0b2b399f75 + checksum: 10c0/d02d45410c9257f5620c766f861f8fa3762b74ef01fdba8060b33a4c98f929e2219cd476b25bd4181ac186158a4d99a0da555c0b6ba45a7ac4a3a5885baad1f5 languageName: node linkType: hard -"@csstools/postcss-color-mix-function@npm:^3.0.10": - version: 3.0.10 - resolution: "@csstools/postcss-color-mix-function@npm:3.0.10" +"@csstools/postcss-color-function@npm:^4.0.12": + version: 4.0.12 + resolution: "@csstools/postcss-color-function@npm:4.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/9505a09a805f52555bd06c8f54d537a99578efe5c7e643c9fdaca8cbb7d74d4d3e07b829c6aed315c75ec5ce113261fb402e01b67e4a423ed39ea8991a6dded0 + checksum: 10c0/a355b04d90f89c8e37a4a23543151558060acc68fb2e7d1c3549bebeeae2b147eec26af1fbc6ee690f0ba4830263f2d181f5331d16d3483b5542be46996fa755 languageName: node linkType: hard -"@csstools/postcss-color-mix-variadic-function-arguments@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:1.0.0" +"@csstools/postcss-color-mix-function@npm:^3.0.12": + version: 3.0.12 + resolution: "@csstools/postcss-color-mix-function@npm:3.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/dd45bd19931cc4780247173b793e5f1e6409b76f92b04fe26e07b0fa048aedc7bcbd92356a558581f695654c2f2d189e1b40b14a9c3f246e86e83b0edf646066 + checksum: 10c0/3e98a5118852083d1f87a3f842f78088192b1f9f08fdf1f3b3ef1e8969e18fdadc1e3bcac3d113a07c8917a7e8fa65fdec55a31df9a1b726c8d7ae89db86e8e5 languageName: node linkType: hard -"@csstools/postcss-content-alt-text@npm:^2.0.6": - version: 2.0.6 - resolution: "@csstools/postcss-content-alt-text@npm:2.0.6" +"@csstools/postcss-color-mix-variadic-function-arguments@npm:^1.0.2": + version: 1.0.2 + resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:1.0.2" dependencies: + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/e7d21002a84d0fba4fe815fb7d3d19b81fb1719a7b6fdd240eb6639d58937b64d6f5c9aa11ffe8a64891a2ed181818cd56d346f58949c2eaa9df7c82ee95ef8e + checksum: 10c0/34073f0f0d33e4958f90763e692955a8e8c678b74284234497c4aa0d2143756e1b3616e0c09832caad498870e227ca0a681316afe3a71224fc40ade0ead1bdd9 + languageName: node + linkType: hard + +"@csstools/postcss-content-alt-text@npm:^2.0.8": + version: 2.0.8 + resolution: "@csstools/postcss-content-alt-text@npm:2.0.8" + dependencies: + "@csstools/css-parser-algorithms": "npm:^3.0.5" + "@csstools/css-tokenizer": "npm:^3.0.4" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" + "@csstools/utilities": "npm:^2.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/4c330cc2a1e434688a62613ecceb1434cd725ce024c1ad8d4a4c76b9839d1f3ea8566a8c6494921e2b46ec7feef6af8ed6548c216dcb8f0feab4b1d52c96228e + languageName: node + linkType: hard + +"@csstools/postcss-contrast-color-function@npm:^2.0.12": + version: 2.0.12 + resolution: "@csstools/postcss-contrast-color-function@npm:2.0.12" + dependencies: + "@csstools/css-color-parser": "npm:^3.1.0" + "@csstools/css-parser-algorithms": "npm:^3.0.5" + "@csstools/css-tokenizer": "npm:^3.0.4" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" + "@csstools/utilities": "npm:^2.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/b783ce948cdf1513ee238e9115b42881a8d3e5d13c16038601b1c470d661cfaeeece4eea29904fb9fcae878bad86f766810fa798a703ab9ad4b0cf276b173f8f languageName: node linkType: hard @@ -1853,59 +2015,59 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-gamut-mapping@npm:^2.0.10": - version: 2.0.10 - resolution: "@csstools/postcss-gamut-mapping@npm:2.0.10" +"@csstools/postcss-gamut-mapping@npm:^2.0.11": + version: 2.0.11 + resolution: "@csstools/postcss-gamut-mapping@npm:2.0.11" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" peerDependencies: postcss: ^8.4 - checksum: 10c0/87cd8289478bf88195469fcf4f80c8fed9e0e5ef76a335a10c4c21582542acb16cced1e00e7da90deaf2e62e383a5c6fe402f429f227c87a2c20e2545a69c537 + checksum: 10c0/490b8ccf10e30879a4415afbdd3646e1cdac3671586b7916855cf47a536f3be75eed014396056bde6528e0cb76d904e79bad78afc0b499e837264cf22519d145 languageName: node linkType: hard -"@csstools/postcss-gradients-interpolation-method@npm:^5.0.10": - version: 5.0.10 - resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.10" +"@csstools/postcss-gradients-interpolation-method@npm:^5.0.12": + version: 5.0.12 + resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/206d079d7679a9609a4fb227ddaf3443d04cff88b55bcfec1cf63c9de372b8720edde8614fc51d2237e4edbff8ce34697f912bc25c2ae41390353fce88455515 + checksum: 10c0/70b3d6c7050ce882ed2281e71eb4493531ae8d55d21899920eeeb6c205d90aaf430419a66235484ccce3a1a1891367dfc0ef772f3866ae3a9d8ec5ddd0cfe894 languageName: node linkType: hard -"@csstools/postcss-hwb-function@npm:^4.0.10": - version: 4.0.10 - resolution: "@csstools/postcss-hwb-function@npm:4.0.10" +"@csstools/postcss-hwb-function@npm:^4.0.12": + version: 4.0.12 + resolution: "@csstools/postcss-hwb-function@npm:4.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/defb9b319b14228307196b9a88e3cbf0acd1d3768b936716dca846875068ad4453e7a2a3d75d1fab5534c8655e9c555e1fa70d30e2c85d68ed2117a7cfe7837c + checksum: 10c0/d0dac34da9d7ac654060b6b27690a419718e990b21ff3e63266ea59934a865bc6aeae8eb8e1ca3e227a8b2a208657e3ab70ccdf0437f1f09d21ab848bbffcaa2 languageName: node linkType: hard -"@csstools/postcss-ic-unit@npm:^4.0.2": - version: 4.0.2 - resolution: "@csstools/postcss-ic-unit@npm:4.0.2" +"@csstools/postcss-ic-unit@npm:^4.0.4": + version: 4.0.4 + resolution: "@csstools/postcss-ic-unit@npm:4.0.4" dependencies: - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/26adb8351143e591080f542d87b223ee5ebc5f33f6d03b217505b249ceb19c46a06732a88000e3a1857ae712a6ea0ffa089a24ad8b8042421490539de5c3d0e8 + checksum: 10c0/20168e70ecb4abf7a69e407d653b6c7c9c82f2c7b1da0920e1d035f62b5ef8552cc7f1b62e0dca318df13c348e79fba862e1a4bb0e9432119a82b10aeb511752 languageName: node linkType: hard @@ -1930,17 +2092,17 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-light-dark-function@npm:^2.0.9": - version: 2.0.9 - resolution: "@csstools/postcss-light-dark-function@npm:2.0.9" +"@csstools/postcss-light-dark-function@npm:^2.0.11": + version: 2.0.11 + resolution: "@csstools/postcss-light-dark-function@npm:2.0.11" dependencies: "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/ee2937f0e5dcaafd10349f0914596e8e1ef6f9d46939c6a6b0e2e63cab0552594e5140bf56e485048c3bca6634dd9673a176c57b9e77001332787f4263835c0f + checksum: 10c0/0175be41bb0044a48bc98d5c55cce41ed6b9ada88253c5f20d0ca17287cba4b429742b458ac5744675b9a286109e13ac51d64e226ab16040d7b051ba64c0c77b languageName: node linkType: hard @@ -2044,29 +2206,50 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-oklab-function@npm:^4.0.10": - version: 4.0.10 - resolution: "@csstools/postcss-oklab-function@npm:4.0.10" +"@csstools/postcss-oklab-function@npm:^4.0.12": + version: 4.0.12 + resolution: "@csstools/postcss-oklab-function@npm:4.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/421d1f2574941c3caecd608588533581fc0766998cc85474008a49b5f1011249cb2be7ef9f21a346fd3895598da18e58860fde06d34b1b833918fa880c41c18f + checksum: 10c0/40d4f51b568c8299c054f8971d0e85fa7da609ba23ce6c84dc17e16bc3838640ed6da75c3886dc9a96a11005773c6e23cba13a5510c781b2d633d07ad7bda6b7 languageName: node linkType: hard -"@csstools/postcss-progressive-custom-properties@npm:^4.1.0": - version: 4.1.0 - resolution: "@csstools/postcss-progressive-custom-properties@npm:4.1.0" +"@csstools/postcss-position-area-property@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-position-area-property@npm:1.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/38f770454d46bfed01d43a3f5e7ac07d3111399b374a7198ae6503cdb6288e410c7b4199f5a7af8f16aeb688216445ade97be417c084313d6c56f55e50d34559 + languageName: node + linkType: hard + +"@csstools/postcss-progressive-custom-properties@npm:^4.2.1": + version: 4.2.1 + resolution: "@csstools/postcss-progressive-custom-properties@npm:4.2.1" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/175081a5c53e37a282f596e01359d4411800e4017c2d389caaa2b7c9b7507a50c5f1ac3d937f27f000be3ac2ac788cad9c1490ec6bc1d4de51331f3cc8ccda8e + checksum: 10c0/56e9a147799719fd5c550c035437693dd50cdfef46d66a4f2ce8f196e1006a096aa47d412710a89c3dc9808068a0a101c7f607a507ed68e925580c6f921e84d5 + languageName: node + linkType: hard + +"@csstools/postcss-property-rule-prelude-list@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-property-rule-prelude-list@npm:1.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^3.0.5" + "@csstools/css-tokenizer": "npm:^3.0.4" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/ae8bbca3a77ca59c21c11899a904f9d9417a19a3359d01dee042e0489b7ddfe7cea13ae275b7e7936d9b0b99c0a13f7f685f962cd63ca3d3d2b6e5eacc293a0d languageName: node linkType: hard @@ -2083,18 +2266,18 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-relative-color-syntax@npm:^3.0.10": - version: 3.0.10 - resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.10" +"@csstools/postcss-relative-color-syntax@npm:^3.0.12": + version: 3.0.12 + resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/de9c41a936a77dab68cdb2dd23a26ba1b92d90bf2a7cf463fada2f2daf6ad0d7394fa2b1ed444f509006992961d993383a34a9afd3a48a9dc67a3793afcd9bb8 + checksum: 10c0/11af386c8193e22c148ac034eee94c56da3060bdbde3196d2d641b088e12de35bef187bcd7d421f9e4d49c4f1cfc28b24e136e62107e02ed7007a3a28f635d06 languageName: node linkType: hard @@ -2135,15 +2318,38 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-text-decoration-shorthand@npm:^4.0.2": - version: 4.0.2 - resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.2" +"@csstools/postcss-syntax-descriptor-syntax-production@npm:^1.0.1": + version: 1.0.1 + resolution: "@csstools/postcss-syntax-descriptor-syntax-production@npm:1.0.1" dependencies: - "@csstools/color-helpers": "npm:^5.0.2" + "@csstools/css-tokenizer": "npm:^3.0.4" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/b9b3d84a50b86b1af1b8b7e56a64d5eebc1c89c323a5263306c5c69ddb05a4d468d7072a7786b0ea6601629035df0089565e9d98d55d0f4eb7201cf7ed1bb3e9 + languageName: node + linkType: hard + +"@csstools/postcss-system-ui-font-family@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-system-ui-font-family@npm:1.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^3.0.5" + "@csstools/css-tokenizer": "npm:^3.0.4" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/6a81761ae3cae643659b1416a7a892cf1505474896193b8abc26cff319cb6b1a20b64c5330d64019fba458e058da3abc9407d0ebf0c102289c0b79ef99b4c6d6 + languageName: node + linkType: hard + +"@csstools/postcss-text-decoration-shorthand@npm:^4.0.3": + version: 4.0.3 + resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.3" + dependencies: + "@csstools/color-helpers": "npm:^5.1.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/01e2f3717e7a42224dc1a746491c55a381cf208cb7588f0308eeefe730675be4c7bb56c0cc557e75999c981e67da7d0b0bb68610635752c89ef251ee435b9cac + checksum: 10c0/f6af7d5dcf599edcf76c5e396ef2d372bbe1c1f3fbaaccd91e91049e64b6ff68b44f459277aef0a8110baca3eaa21275012adc52ccb8c0fc526a4c35577f8fce languageName: node linkType: hard @@ -2196,31 +2402,31 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.4.3": - version: 1.4.3 - resolution: "@emnapi/core@npm:1.4.3" +"@emnapi/core@npm:^1.7.1": + version: 1.8.0 + resolution: "@emnapi/core@npm:1.8.0" dependencies: - "@emnapi/wasi-threads": "npm:1.0.2" + "@emnapi/wasi-threads": "npm:1.1.0" tslib: "npm:^2.4.0" - checksum: 10c0/e30101d16d37ef3283538a35cad60e22095aff2403fb9226a35330b932eb6740b81364d525537a94eb4fb51355e48ae9b10d779c0dd1cdcd55d71461fe4b45c7 + checksum: 10c0/4bb07df0a6ba6478bd4bee2a3609df1f9ef835588227261e9d8ef40f25a9e7596d837f663a4b2c2193d7e8a130370cab5a8063255d81c30903a8c167435d2687 languageName: node linkType: hard -"@emnapi/runtime@npm:^1.4.3": - version: 1.4.3 - resolution: "@emnapi/runtime@npm:1.4.3" +"@emnapi/runtime@npm:^1.7.1": + version: 1.8.0 + resolution: "@emnapi/runtime@npm:1.8.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/3b7ab72d21cb4e034f07df80165265f85f445ef3f581d1bc87b67e5239428baa00200b68a7d5e37a0425c3a78320b541b07f76c5530f6f6f95336a6294ebf30b + checksum: 10c0/25330ade92c48d19f4dc896ecfe906a45c95bd2d3d4155fcf4934e796703f90f23c8f2e7466be44ec734b68ea3e50f64831647d75e1bd44aebbbe1ca870f932a languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.0.2": - version: 1.0.2 - resolution: "@emnapi/wasi-threads@npm:1.0.2" +"@emnapi/wasi-threads@npm:1.1.0": + version: 1.1.0 + resolution: "@emnapi/wasi-threads@npm:1.1.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/f0621b1fc715221bd2d8332c0ca922617bcd77cdb3050eae50a124eb8923c54fa425d23982dc8f29d505c8798a62d1049bace8b0686098ff9dd82270e06d772e + checksum: 10c0/e6d54bf2b1e64cdd83d2916411e44e579b6ae35d5def0dea61a3c452d9921373044dff32a8b8473ae60c80692bdc39323e98b96a3f3d87ba6886b24dd0ef7ca1 languageName: node linkType: hard @@ -2231,6 +2437,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/aix-ppc64@npm:0.27.2" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/android-arm64@npm:0.25.1" @@ -2238,6 +2451,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm64@npm:0.27.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/android-arm@npm:0.25.1" @@ -2245,6 +2465,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm@npm:0.27.2" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/android-x64@npm:0.25.1" @@ -2252,6 +2479,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-x64@npm:0.27.2" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/darwin-arm64@npm:0.25.1" @@ -2259,6 +2493,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-arm64@npm:0.27.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/darwin-x64@npm:0.25.1" @@ -2266,6 +2507,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-x64@npm:0.27.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/freebsd-arm64@npm:0.25.1" @@ -2273,6 +2521,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-arm64@npm:0.27.2" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/freebsd-x64@npm:0.25.1" @@ -2280,6 +2535,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-x64@npm:0.27.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-arm64@npm:0.25.1" @@ -2287,6 +2549,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm64@npm:0.27.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-arm@npm:0.25.1" @@ -2294,6 +2563,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm@npm:0.27.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-ia32@npm:0.25.1" @@ -2301,6 +2577,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ia32@npm:0.27.2" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-loong64@npm:0.25.1" @@ -2308,6 +2591,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-loong64@npm:0.27.2" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-mips64el@npm:0.25.1" @@ -2315,6 +2605,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-mips64el@npm:0.27.2" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-ppc64@npm:0.25.1" @@ -2322,6 +2619,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ppc64@npm:0.27.2" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-riscv64@npm:0.25.1" @@ -2329,6 +2633,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-riscv64@npm:0.27.2" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-s390x@npm:0.25.1" @@ -2336,6 +2647,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-s390x@npm:0.27.2" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/linux-x64@npm:0.25.1" @@ -2343,6 +2661,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-x64@npm:0.27.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/netbsd-arm64@npm:0.25.1" @@ -2350,6 +2675,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-arm64@npm:0.27.2" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/netbsd-x64@npm:0.25.1" @@ -2357,6 +2689,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-x64@npm:0.27.2" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/openbsd-arm64@npm:0.25.1" @@ -2364,6 +2703,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-arm64@npm:0.27.2" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/openbsd-x64@npm:0.25.1" @@ -2371,6 +2717,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-x64@npm:0.27.2" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openharmony-arm64@npm:0.27.2" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/sunos-x64@npm:0.25.1" @@ -2378,6 +2738,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/sunos-x64@npm:0.27.2" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/win32-arm64@npm:0.25.1" @@ -2385,6 +2752,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-arm64@npm:0.27.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/win32-ia32@npm:0.25.1" @@ -2392,6 +2766,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-ia32@npm:0.27.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.1": version: 0.25.1 resolution: "@esbuild/win32-x64@npm:0.25.1" @@ -2399,6 +2780,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-x64@npm:0.27.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -2530,28 +2918,40 @@ __metadata: linkType: hard "@fontsource/inconsolata@npm:^5.1.0": - version: 5.2.6 - resolution: "@fontsource/inconsolata@npm:5.2.6" - checksum: 10c0/3e76e02ca075ed81d4e1f7b3de39051cf98a1c5e0acc5ef348f0e2fd5ba9047c79fefc245cc78dbfa98b0fb4dafe8882cd3df57a451c02b104ccc877cccf8433 + version: 5.2.8 + resolution: "@fontsource/inconsolata@npm:5.2.8" + checksum: 10c0/2d5788a41bc60d7d00e5ba75689241a5146a4f60a3ec79d14dd2a0a5aa1ec2e697aa6aed3d1c0564208d32916aea588535f16a57f7a775b554d7f9adb03ad64a languageName: node linkType: hard "@fontsource/inter@npm:^5.1.0": - version: 5.2.6 - resolution: "@fontsource/inter@npm:5.2.6" - checksum: 10c0/7a1347608aab06e53665272ed41fd906ffa96c57b004e8aae91b98177bba97c54b60305d956254c4771387c383f9bdbd917006fb34c9721c020e6017d33e9a5c + version: 5.2.8 + resolution: "@fontsource/inter@npm:5.2.8" + checksum: 10c0/f737dd50005e4809887ba55ae0c9b7174216d6d14875d17a4fbb9a0ad75dec4265928b805a43fe16a23f14a878f1974a398bbfc84ad65c79fc4d4b9c3ea154e1 languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.3.4": - version: 2.3.4 - resolution: "@formatjs/ecma402-abstract@npm:2.3.4" +"@formatjs/ecma402-abstract@npm:2.3.6": + version: 2.3.6 + resolution: "@formatjs/ecma402-abstract@npm:2.3.6" dependencies: "@formatjs/fast-memoize": "npm:2.2.7" - "@formatjs/intl-localematcher": "npm:0.6.1" + "@formatjs/intl-localematcher": "npm:0.6.2" decimal.js: "npm:^10.4.3" tslib: "npm:^2.8.0" - checksum: 10c0/2644bc618a34dc610ef9691281eeb45ae6175e6982cf19f1bd140672fc95c748747ce3c85b934649ea7e4a304f7ae0060625fd53d5df76f92ca3acf743e1eb0a + checksum: 10c0/63be2a73d3168bf45ab5d50db58376e852db5652d89511ae6e44f1fa03ad96ebbfe9b06a1dfaa743db06e40eb7f33bd77530b9388289855cca79a0e3fc29eacf + languageName: node + linkType: hard + +"@formatjs/ecma402-abstract@npm:3.0.7": + version: 3.0.7 + resolution: "@formatjs/ecma402-abstract@npm:3.0.7" + dependencies: + "@formatjs/fast-memoize": "npm:3.0.2" + "@formatjs/intl-localematcher": "npm:0.7.4" + decimal.js: "npm:^10.4.3" + tslib: "npm:^2.8.0" + checksum: 10c0/0fdc25ef72dcd5bbe1deeb190be2f0a2e2770a2135904d16ddfb424305a1efed14b026fba6c48121bc32f693abf1fe08c0ee12cb7d888cb2ba92963236d82c77 languageName: node linkType: hard @@ -2564,34 +2964,53 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-durationformat@npm:^0.7.0": - version: 0.7.4 - resolution: "@formatjs/intl-durationformat@npm:0.7.4" +"@formatjs/fast-memoize@npm:3.0.2": + version: 3.0.2 + resolution: "@formatjs/fast-memoize@npm:3.0.2" dependencies: - "@formatjs/ecma402-abstract": "npm:2.3.4" - "@formatjs/intl-localematcher": "npm:0.6.1" tslib: "npm:^2.8.0" - checksum: 10c0/e340cab41fcb52639c73f25a422a2cf252c0fb8d328b88bd72b8f995e2d7ffa5d6aa7bb4f4e11862d45bd7eef398d9c8a1cf9d400bcf89c42ff3c38e63196dbe + checksum: 10c0/f7d1074090df309d37322979fe5fc96451531317b42bd927102a3a86dee537b1cb0e378158c74e00efd9714a0aa0f1e5a673c749535df200e13167112676ce88 languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.6.1": - version: 0.6.1 - resolution: "@formatjs/intl-localematcher@npm:0.6.1" +"@formatjs/intl-durationformat@npm:^0.9.0": + version: 0.9.1 + resolution: "@formatjs/intl-durationformat@npm:0.9.1" + dependencies: + "@formatjs/ecma402-abstract": "npm:3.0.7" + "@formatjs/intl-localematcher": "npm:0.7.4" + tslib: "npm:^2.8.0" + checksum: 10c0/6f7b01027c07162b26be3014bba17a7633d1f9cfe6c26c5f403e72b92ac26c67cda2d88aeedab891b080664cfb4aace0eacec70b6f006616fe0d322dc2e8145d + languageName: node + linkType: hard + +"@formatjs/intl-localematcher@npm:0.6.2": + version: 0.6.2 + resolution: "@formatjs/intl-localematcher@npm:0.6.2" dependencies: tslib: "npm:^2.8.0" - checksum: 10c0/bacbedd508519c1bb5ca2620e89dc38f12101be59439aa14aa472b222915b462cb7d679726640f6dcf52a05dd218b5aa27ccd60f2e5010bb96f1d4929848cde0 + checksum: 10c0/22a17a4c67160b6c9f52667914acfb7b79cd6d80630d4ac6d4599ce447cb89d2a64f7d58fa35c3145ddb37fef893f0a45b9a55e663a4eb1f2ae8b10a89fac235 + languageName: node + linkType: hard + +"@formatjs/intl-localematcher@npm:0.7.4": + version: 0.7.4 + resolution: "@formatjs/intl-localematcher@npm:0.7.4" + dependencies: + "@formatjs/fast-memoize": "npm:3.0.2" + tslib: "npm:^2.8.0" + checksum: 10c0/7fc31e13397317faadee033dcf668cda49f031b28542c634c920339f374f483235543e08be2077152cfe5dd41e651d8d2d37b6ece8aa044c0998c48f5472fb1a languageName: node linkType: hard "@formatjs/intl-segmenter@npm:^11.7.3": - version: 11.7.10 - resolution: "@formatjs/intl-segmenter@npm:11.7.10" + version: 11.7.12 + resolution: "@formatjs/intl-segmenter@npm:11.7.12" dependencies: - "@formatjs/ecma402-abstract": "npm:2.3.4" - "@formatjs/intl-localematcher": "npm:0.6.1" + "@formatjs/ecma402-abstract": "npm:2.3.6" + "@formatjs/intl-localematcher": "npm:0.6.2" tslib: "npm:^2.8.0" - checksum: 10c0/f84974d00a020cf9f7c153c56f2bb20a104b1cce33c6672c491c842f2b2367d6746bc903a7f18e922da1a89c7ee6edd9e79bf6971dd26c41b6c9faaa76e6c57e + checksum: 10c0/5ab30a9b9e9a63b9c29c3b90a94e7e5eef958829ed0b03a0117998fe0c1e213546eeebe0636b4559d27db86868bffc5311e3619248a2416045c99de6982fa46a languageName: node linkType: hard @@ -2680,6 +3099,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/remapping@npm:^2.3.5": + version: 2.3.5 + resolution: "@jridgewell/remapping@npm:2.3.5" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/3de494219ffeb2c5c38711d0d7bb128097edf91893090a2dbc8ee0b55d092bb7347b1fd0f478486c5eab010e855c73927b1666f2107516d472d24a73017d1194 + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" @@ -2781,7 +3210,7 @@ __metadata: languageName: node linkType: hard -"@livekit/protocol@npm:1.42.2, @livekit/protocol@npm:^1.42.2": +"@livekit/protocol@npm:1.42.2": version: 1.42.2 resolution: "@livekit/protocol@npm:1.42.2" dependencies: @@ -2790,15 +3219,24 @@ __metadata: languageName: node linkType: hard -"@livekit/track-processors@npm:^0.5.5": - version: 0.5.8 - resolution: "@livekit/track-processors@npm:0.5.8" +"@livekit/protocol@npm:^1.42.2": + version: 1.43.4 + resolution: "@livekit/protocol@npm:1.43.4" + dependencies: + "@bufbuild/protobuf": "npm:^1.10.0" + checksum: 10c0/38077ceec44151b7481a95ce25869570b1466359de4992d9367002fc5b0925fc8ca120ed448099ae552064f23664ebe0920669f4fba97164eacbf181664683f2 + languageName: node + linkType: hard + +"@livekit/track-processors@npm:^0.7.0": + version: 0.7.0 + resolution: "@livekit/track-processors@npm:0.7.0" dependencies: "@mediapipe/tasks-vision": "npm:0.10.14" peerDependencies: "@types/dom-mediacapture-transform": ^0.1.9 livekit-client: ^1.12.0 || ^2.1.0 - checksum: 10c0/1ea87fd79b518a7736b5c0052522ba31cc161fe1181ac0d438fa8a01a25347729b375a4865ea8da3422a922a4cffbc6cf58af13141d79b75f5ea19196e2b97b4 + checksum: 10c0/4c1ec427586e885c44d2865a98008b563d002b1b98d117383637a696597d71a0ff64d8a5bcba48033298e5c2cbaa9e357481e8a4a182982a355eb9e0eeb87643 languageName: node linkType: hard @@ -2816,14 +3254,14 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^0.2.11": - version: 0.2.11 - resolution: "@napi-rs/wasm-runtime@npm:0.2.11" +"@napi-rs/wasm-runtime@npm:^1.1.0": + version: 1.1.1 + resolution: "@napi-rs/wasm-runtime@npm:1.1.1" dependencies: - "@emnapi/core": "npm:^1.4.3" - "@emnapi/runtime": "npm:^1.4.3" - "@tybys/wasm-util": "npm:^0.9.0" - checksum: 10c0/049bd14c58b99fbe0967b95e9921c5503df196b59be22948d2155f17652eb305cff6728efd8685338b855da7e476dd2551fbe3a313fc2d810938f0717478441e + "@emnapi/core": "npm:^1.7.1" + "@emnapi/runtime": "npm:^1.7.1" + "@tybys/wasm-util": "npm:^0.10.1" + checksum: 10c0/04d57b67e80736e41fe44674a011878db0a8ad893f4d44abb9d3608debb7c174224cba2796ed5b0c1d367368159f3ca6be45f1c59222f70e32ddc880f803d447 languageName: node linkType: hard @@ -2996,12 +3434,12 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/api-logs@npm:0.203.0" +"@opentelemetry/api-logs@npm:0.208.0": + version: 0.208.0 + resolution: "@opentelemetry/api-logs@npm:0.208.0" dependencies: "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/e7a0a0ff46aaeb62192a99f45ef4889222e4fea09be25cab6fea811afc2df95c02ea050b2c98dfc0fc5a6ec6a623d87096af2751fdf91ddbb3afcab61b5325da + checksum: 10c0/dc1fbee6219df4166509f43b74ea936bb18b6d594565b0bcf56b654a1c958b50d6046b8739dc36c98149fe890c02150ff3814e963f5ea439a07ff3c562555b99 languageName: node linkType: hard @@ -3012,219 +3450,268 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:2.0.1, @opentelemetry/core@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/core@npm:2.0.1" +"@opentelemetry/core@npm:2.2.0, @opentelemetry/core@npm:^2.0.0": + version: 2.2.0 + resolution: "@opentelemetry/core@npm:2.2.0" dependencies: "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8 + checksum: 10c0/f618b63f2f560d052791d2406b1411722aa4b0585031242e6906f869f0a707ffe725c4b29bf18aed1f202e1ab5dfc3a9f769c517ac8521338b33ac8c4265fba9 languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-http@npm:^0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.203.0" +"@opentelemetry/exporter-trace-otlp-http@npm:^0.208.0": + version: 0.208.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.208.0" dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-exporter-base": "npm:0.203.0" - "@opentelemetry/otlp-transformer": "npm:0.203.0" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/otlp-exporter-base": "npm:0.208.0" + "@opentelemetry/otlp-transformer": "npm:0.208.0" + "@opentelemetry/resources": "npm:2.2.0" + "@opentelemetry/sdk-trace-base": "npm:2.2.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/21a65ebc40dcab05cf11178e5037f96847ce344c4a855aac46dcab3f74982016318ee75fafdfeeb42f10b92a0a781b7cd8b2b5b036cbe53c14714fd13940142e + checksum: 10c0/5e901388febb6e797aa7ed1705373df322df4ba47eaf545a85bb4e1e3b3056993a455d9d6a68e94dc57e6c8112d580129b374bf2982595244edf664663b93e66 languageName: node linkType: hard -"@opentelemetry/otlp-exporter-base@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.203.0" +"@opentelemetry/otlp-exporter-base@npm:0.208.0": + version: 0.208.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.208.0" dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/otlp-transformer": "npm:0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/ad5b771b06b192f06f332f60701d1ad208df88a05975b16e1cdd1dff8e1cb66e775b3e9de513c2f5d48f390f25ca35411ead08ce4849c8203b86a264d34561d3 + checksum: 10c0/c2b2014da16e2a2be0ebe525b1a62b3e64e286fc9c2575444e4c75bbe0060a83762172180dc7a97cdaaaa8c6765076073edea30340459fc1820cd43468ff98b0 languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.203.0" +"@opentelemetry/otlp-transformer@npm:0.208.0": + version: 0.208.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.208.0" dependencies: - "@opentelemetry/api-logs": "npm:0.203.0" - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-logs": "npm:0.203.0" - "@opentelemetry/sdk-metrics": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/api-logs": "npm:0.208.0" + "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/resources": "npm:2.2.0" + "@opentelemetry/sdk-logs": "npm:0.208.0" + "@opentelemetry/sdk-metrics": "npm:2.2.0" + "@opentelemetry/sdk-trace-base": "npm:2.2.0" protobufjs: "npm:^7.3.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/3f7b4bfe4bcab4db434ff2c4e59b53de53642d379b80056610456d8e9ae0cbab0f8b69f088078637b7b5ceffd0ac2fda68469c5f295b1c0ac625f522f640338c + checksum: 10c0/70c04b2a52f0b2f8aece25ad21401c32ed3136ccd6e82b767d570a24d5456a5ded206ed4cc60ebc09eac08a4aa9c03bc8dcbf10730e491f1af3e7768c361ac12 languageName: node linkType: hard -"@opentelemetry/resources@npm:2.0.1, @opentelemetry/resources@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/resources@npm:2.0.1" +"@opentelemetry/resources@npm:2.2.0, @opentelemetry/resources@npm:^2.0.0": + version: 2.2.0 + resolution: "@opentelemetry/resources@npm:2.2.0" dependencies: - "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/core": "npm:2.2.0" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341 + checksum: 10c0/f08fa69ccccb6d14b6932fabe6f8e097c0dfc41ae8f4c0f6c54fb04bc3d9c04e742da3e22d7240d74b585287101126d97a0da192b493a9724dc07a56ca1b77e0 languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.203.0": - version: 0.203.0 - resolution: "@opentelemetry/sdk-logs@npm:0.203.0" +"@opentelemetry/sdk-logs@npm:0.208.0": + version: 0.208.0 + resolution: "@opentelemetry/sdk-logs@npm:0.208.0" dependencies: - "@opentelemetry/api-logs": "npm:0.203.0" - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/api-logs": "npm:0.208.0" + "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/resources": "npm:2.2.0" peerDependencies: "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/02dd9d9969628f05f71ae1d149f1aa6d1fee2dad607923a68a1cfc923e94b046dcc0e18e85e865324e3bda0cee7a5a0ba9fa0d57e4e95fa672be103e2ce60270 + checksum: 10c0/a167ee7d2818e435ff7480836461f94543e4e39f0e8e8013d462c635def9b960dcf1a29e5536743946b51ef13b764f518d9edb511e89bc1e8995acc96f54241f languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:2.0.1": - version: 2.0.1 - resolution: "@opentelemetry/sdk-metrics@npm:2.0.1" +"@opentelemetry/sdk-metrics@npm:2.2.0": + version: 2.2.0 + resolution: "@opentelemetry/sdk-metrics@npm:2.2.0" dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/resources": "npm:2.2.0" peerDependencies: "@opentelemetry/api": ">=1.9.0 <1.10.0" - checksum: 10c0/fcf7ae23d459e5da7cb6fe150064b6dc4e11e47925b08980c3b357bd5534ad388898bbacd0ff8befef6801f43b35142dc7123f028ffde2d0fe2bd72177d07639 + checksum: 10c0/a2668f9ef937123552a5ab96ec23675931ae7d3223ec7a31c8aac95fbbfb0b03a54a873f17f2356b04db7031421e7e3d7e3bf9d96d9069a0b97c680a2c158bc4 languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:2.0.1, @opentelemetry/sdk-trace-base@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/sdk-trace-base@npm:2.0.1" +"@opentelemetry/sdk-trace-base@npm:2.2.0, @opentelemetry/sdk-trace-base@npm:^2.0.0": + version: 2.2.0 + resolution: "@opentelemetry/sdk-trace-base@npm:2.2.0" dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/resources": "npm:2.2.0" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b + checksum: 10c0/a67715b71d7253cd61ea79954f56491796ac7a660d03d5381fd81defd4546042bb465b27e1b6eee4b1ed32c00305a5349a16d04fd44314c9a1d371a0a638107a languageName: node linkType: hard "@opentelemetry/sdk-trace-web@npm:^2.0.0": - version: 2.0.1 - resolution: "@opentelemetry/sdk-trace-web@npm:2.0.1" + version: 2.2.0 + resolution: "@opentelemetry/sdk-trace-web@npm:2.2.0" dependencies: - "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/sdk-trace-base": "npm:2.2.0" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/48821b91430e24378b0b5b2632e78efdd018a3f840462a6aeba6ce318a6480bad2f623cc7f7f625a9266028ad44b78eb8456181778de6cb18725f26c44e2729b + checksum: 10c0/002296bb929d4992575415ea93c2a68411cd5018c6e7fa27b7cd48e78741b7eddfb87eb0903e7ca42740acb04ac4e8508d00f7651bdc63b433055af955201d31 languageName: node linkType: hard "@opentelemetry/semantic-conventions@npm:^1.25.1, @opentelemetry/semantic-conventions@npm:^1.29.0": - version: 1.36.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.36.0" - checksum: 10c0/edc8a6fe3ec4fc0c67ba3a92b86fb3dcc78fe1eb4f19838d8013c3232b9868540a034dd25cfe0afdd5eae752c5f0e9f42272ff46da144a2d5b35c644478e1c62 + version: 1.38.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.38.0" + checksum: 10c0/ae93e39ac18bf47df2b11d43e9a0dc1673b9d33e5f1e7f357c92968e6329fb9a67cf8a447e9a7150948ee3f8178b38274db365b8fa775a8c54802e0c6ccdd2ca languageName: node linkType: hard -"@oxc-resolver/binding-darwin-arm64@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.3.0" +"@oxc-resolver/binding-android-arm-eabi@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.16.2" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@oxc-resolver/binding-android-arm64@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-android-arm64@npm:11.16.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-resolver/binding-darwin-arm64@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.16.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-darwin-x64@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-darwin-x64@npm:11.3.0" +"@oxc-resolver/binding-darwin-x64@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-darwin-x64@npm:11.16.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-resolver/binding-freebsd-x64@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.3.0" +"@oxc-resolver/binding-freebsd-x64@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.16.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.3.0" +"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.16.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm64-gnu@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.3.0" +"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.16.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@oxc-resolver/binding-linux-arm64-gnu@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.16.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm64-musl@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.3.0" +"@oxc-resolver/binding-linux-arm64-musl@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.16.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.3.0" +"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.16.2" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.16.2" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-s390x-gnu@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.3.0" +"@oxc-resolver/binding-linux-riscv64-musl@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.16.2" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@oxc-resolver/binding-linux-s390x-gnu@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.16.2" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-x64-gnu@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.3.0" +"@oxc-resolver/binding-linux-x64-gnu@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.16.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-x64-musl@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.3.0" +"@oxc-resolver/binding-linux-x64-musl@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.16.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-wasm32-wasi@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.3.0" +"@oxc-resolver/binding-openharmony-arm64@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.16.2" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-resolver/binding-wasm32-wasi@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.16.2" dependencies: - "@napi-rs/wasm-runtime": "npm:^0.2.11" + "@napi-rs/wasm-runtime": "npm:^1.1.0" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-resolver/binding-win32-arm64-msvc@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.3.0" +"@oxc-resolver/binding-win32-arm64-msvc@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.16.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-win32-x64-msvc@npm:11.3.0": - version: 11.3.0 - resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.3.0" +"@oxc-resolver/binding-win32-ia32-msvc@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-win32-ia32-msvc@npm:11.16.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@oxc-resolver/binding-win32-x64-msvc@npm:11.16.2": + version: 11.16.2 + resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.16.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3478,10 +3965,10 @@ __metadata: languageName: node linkType: hard -"@radix-ui/primitive@npm:1.1.2": - version: 1.1.2 - resolution: "@radix-ui/primitive@npm:1.1.2" - checksum: 10c0/5e2d2528d2fe37c16865e77b0beaac2b415a817ad13d8178db6e8187b2a092672568a64ee0041510abfde3034490a5cadd3057049bb15789020c06892047597c +"@radix-ui/primitive@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/primitive@npm:1.1.3" + checksum: 10c0/88860165ee7066fa2c179f32ffcd3ee6d527d9dcdc0e8be85e9cb0e2c84834be8e3c1a976c74ba44b193f709544e12f54455d892b28e32f0708d89deda6b9f1d languageName: node linkType: hard @@ -3625,18 +4112,18 @@ __metadata: linkType: hard "@radix-ui/react-dialog@npm:^1.0.4, @radix-ui/react-dialog@npm:^1.1.1": - version: 1.1.14 - resolution: "@radix-ui/react-dialog@npm:1.1.14" + version: 1.1.15 + resolution: "@radix-ui/react-dialog@npm:1.1.15" dependencies: - "@radix-ui/primitive": "npm:1.1.2" + "@radix-ui/primitive": "npm:1.1.3" "@radix-ui/react-compose-refs": "npm:1.1.2" "@radix-ui/react-context": "npm:1.1.2" - "@radix-ui/react-dismissable-layer": "npm:1.1.10" - "@radix-ui/react-focus-guards": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-focus-guards": "npm:1.1.3" "@radix-ui/react-focus-scope": "npm:1.1.7" "@radix-ui/react-id": "npm:1.1.1" "@radix-ui/react-portal": "npm:1.1.9" - "@radix-ui/react-presence": "npm:1.1.4" + "@radix-ui/react-presence": "npm:1.1.5" "@radix-ui/react-primitive": "npm:2.1.3" "@radix-ui/react-slot": "npm:1.2.3" "@radix-ui/react-use-controllable-state": "npm:1.2.2" @@ -3652,7 +4139,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/ab7bc783510ed8fccfe91020b214f4a571d5a1d46d398faa33f4c151bc9f586c47483b307e72b67687b06694c194b3aa80dd1de728460fa765db9f3057690ba3 + checksum: 10c0/2f2c88e3c281acaea2fd9b96fa82132d59177d3aa5da2e7c045596fd4028e84e44ac52ac28f4f236910605dd7d9338c2858ba44a9ced2af2e3e523abbfd33014 languageName: node linkType: hard @@ -3682,11 +4169,11 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-dismissable-layer@npm:1.1.10": - version: 1.1.10 - resolution: "@radix-ui/react-dismissable-layer@npm:1.1.10" +"@radix-ui/react-dismissable-layer@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.11" dependencies: - "@radix-ui/primitive": "npm:1.1.2" + "@radix-ui/primitive": "npm:1.1.3" "@radix-ui/react-compose-refs": "npm:1.1.2" "@radix-ui/react-primitive": "npm:2.1.3" "@radix-ui/react-use-callback-ref": "npm:1.1.1" @@ -3701,7 +4188,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/21a2d03689f5e06586135b6a735937ef14f2571fdf6044a3019bc3f9fa368a9400b5a9b631f43e8ad3682693449e369ffa7cc8642764246ce18ebe7359a45faf + checksum: 10c0/c825572a64073c4d3853702029979f6658770ffd6a98eabc4984e1dee1b226b4078a2a4dc7003f96475b438985e9b21a58e75f51db74dd06848dcae1f2d395dc languageName: node linkType: hard @@ -3766,16 +4253,16 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-focus-guards@npm:1.1.2": - version: 1.1.2 - resolution: "@radix-ui/react-focus-guards@npm:1.1.2" +"@radix-ui/react-focus-guards@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-focus-guards@npm:1.1.3" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/8d6fa55752b9b6e55d1eebb643178e38a824e8ba418eb29031b2979077a12c4e3922892de9f984dd326f77071a14960cd81e99a960beea07598b8c80da618dc5 + checksum: 10c0/0bab65eb8d7e4f72f685d63de7fbba2450e3cb15ad6a20a16b42195e9d335c576356f5a47cb58d1ffc115393e46d7b14b12c5d4b10029b0ec090861255866985 languageName: node linkType: hard @@ -4018,9 +4505,9 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-presence@npm:1.1.4": - version: 1.1.4 - resolution: "@radix-ui/react-presence@npm:1.1.4" +"@radix-ui/react-presence@npm:1.1.5": + version: 1.1.5 + resolution: "@radix-ui/react-presence@npm:1.1.5" dependencies: "@radix-ui/react-compose-refs": "npm:1.1.2" "@radix-ui/react-use-layout-effect": "npm:1.1.1" @@ -4034,7 +4521,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/8202647139d6f5097b0abcc43dfba471c00b69da95ca336afe3ea23a165e05ca21992f40fc801760fe442f3e064e54e2f2cbcb9ad758c4b07ef6c69a5b6777bd + checksum: 10c0/d0e61d314250eeaef5369983cb790701d667f51734bafd98cf759072755562018052c594e6cdc5389789f4543cb0a4d98f03ff4e8f37338d6b5bf51a1700c1d1 languageName: node linkType: hard @@ -4076,6 +4563,25 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-primitive@npm:2.1.4": + version: 2.1.4 + resolution: "@radix-ui/react-primitive@npm:2.1.4" + dependencies: + "@radix-ui/react-slot": "npm:1.2.4" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/90d687b222a25975371ed1f9f08648d75237214b8dec4cbaf09ec9ac951339b17421278f1aff2fb7c5672ba8bd03774a94904efdba73805dd5cc947ce5be8c4a + languageName: node + linkType: hard + "@radix-ui/react-progress@npm:^1.1.0": version: 1.1.1 resolution: "@radix-ui/react-progress@npm:1.1.1" @@ -4143,11 +4649,11 @@ __metadata: linkType: hard "@radix-ui/react-slider@npm:^1.1.2": - version: 1.3.5 - resolution: "@radix-ui/react-slider@npm:1.3.5" + version: 1.3.6 + resolution: "@radix-ui/react-slider@npm:1.3.6" dependencies: "@radix-ui/number": "npm:1.1.1" - "@radix-ui/primitive": "npm:1.1.2" + "@radix-ui/primitive": "npm:1.1.3" "@radix-ui/react-collection": "npm:1.1.7" "@radix-ui/react-compose-refs": "npm:1.1.2" "@radix-ui/react-context": "npm:1.1.2" @@ -4167,7 +4673,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/2f5f37f953d78ac02ed74120afe76badf3a7d0e19036f4de6cdeda38220718a1d5113ffc2f43e0b3de73e14564cae9a09d1968ee3f9641625849d126a036717f + checksum: 10c0/a53d7854e28c5ef3d29b76c8d04cc3c723b982b643152cd5a8fefc7a8359180f8fd21753e5a08302a290bc837e7be04f2efad9d308b7a4a23326df6a6b1ac882 languageName: node linkType: hard @@ -4201,6 +4707,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-slot@npm:1.2.4": + version: 1.2.4 + resolution: "@radix-ui/react-slot@npm:1.2.4" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/8b719bb934f1ae5ac0e37214783085c17c2f1080217caf514c1c6cc3d9ca56c7e19d25470b26da79aa6e605ab36589edaade149b76f5fc0666f1063e2fc0a0dc + languageName: node + linkType: hard + "@radix-ui/react-use-callback-ref@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-use-callback-ref@npm:1.1.0" @@ -4388,10 +4909,10 @@ __metadata: linkType: hard "@radix-ui/react-visually-hidden@npm:^1.0.3": - version: 1.2.3 - resolution: "@radix-ui/react-visually-hidden@npm:1.2.3" + version: 1.2.4 + resolution: "@radix-ui/react-visually-hidden@npm:1.2.4" dependencies: - "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-primitive": "npm:2.1.4" peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -4402,7 +4923,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/cf86a37f1cbee50a964056f3dc4f6bb1ee79c76daa321f913aa20ff3e1ccdfafbf2b114d7bb616aeefc7c4b895e6ca898523fdb67710d89bd5d8edb739a0d9b6 + checksum: 10c0/cca313cd3268f483612da1ab91c4cca55a54d24963dd543154f2d043bfdca21a96ab0582152ae473de44769474867d5433dbadae799a42932e6204fd2d5fa889 languageName: node linkType: hard @@ -4413,69 +4934,69 @@ __metadata: languageName: node linkType: hard -"@react-spring/animated@npm:~10.0.1": - version: 10.0.1 - resolution: "@react-spring/animated@npm:10.0.1" +"@react-spring/animated@npm:~10.0.3": + version: 10.0.3 + resolution: "@react-spring/animated@npm:10.0.3" dependencies: - "@react-spring/shared": "npm:~10.0.1" - "@react-spring/types": "npm:~10.0.1" + "@react-spring/shared": "npm:~10.0.3" + "@react-spring/types": "npm:~10.0.3" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/aaccd4a8b0280ac846d463b253ad8f092ee4afc9dbedc8e77616adf5399ffec755344f09fdd8487cadaf815840dff84d354d1143579c27c2fcd6937549b5fc40 + checksum: 10c0/6142522f310926729a92009a108ec1bd0d1fe3dca7e9aa0c49692c82fbed28f70c0ea9808fc7452f04bb688ae34333e780f794495906fa4b0efbfc7d53a19b6b languageName: node linkType: hard -"@react-spring/core@npm:~10.0.1": - version: 10.0.1 - resolution: "@react-spring/core@npm:10.0.1" +"@react-spring/core@npm:~10.0.3": + version: 10.0.3 + resolution: "@react-spring/core@npm:10.0.3" dependencies: - "@react-spring/animated": "npm:~10.0.1" - "@react-spring/shared": "npm:~10.0.1" - "@react-spring/types": "npm:~10.0.1" + "@react-spring/animated": "npm:~10.0.3" + "@react-spring/shared": "npm:~10.0.3" + "@react-spring/types": "npm:~10.0.3" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/059b122dda4138e5e7e461abd49350921e326735ca9a1d8aa19b1fdbae0937661b5f71af6fe82fd8f59e8db5549627849b38cc3f7ef2ec7ee9c93c3d6225174f + checksum: 10c0/d941541d4a40a5229f488e78b414149d54238065178fdd14db307a851d285d521ab1914c0d426b102e0190651dbe752aeb743cee4cd497f5c066be937f2d1790 languageName: node linkType: hard -"@react-spring/rafz@npm:~10.0.1": - version: 10.0.1 - resolution: "@react-spring/rafz@npm:10.0.1" - checksum: 10c0/cba76f143d3a06f79dd0c09f7aefd17df9cca9b2c1ef7f9103255e5351326f4a42a5a1366f731a78f74380d96ba683bcc2a49312ed1e4b9e9e249e72c9ff68cb +"@react-spring/rafz@npm:~10.0.3": + version: 10.0.3 + resolution: "@react-spring/rafz@npm:10.0.3" + checksum: 10c0/4cf6f710e2be64a3d94e90a20a24a93c68a89b618538d32aaf0079217f9fbed610395051a181d2d010c7ed6898cb7239a3f2ced1d91dd93e4138563ffd2d44ce languageName: node linkType: hard -"@react-spring/shared@npm:~10.0.1": - version: 10.0.1 - resolution: "@react-spring/shared@npm:10.0.1" +"@react-spring/shared@npm:~10.0.3": + version: 10.0.3 + resolution: "@react-spring/shared@npm:10.0.3" dependencies: - "@react-spring/rafz": "npm:~10.0.1" - "@react-spring/types": "npm:~10.0.1" + "@react-spring/rafz": "npm:~10.0.3" + "@react-spring/types": "npm:~10.0.3" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/f056aaa018b3744afd8244e8eea24534d32f92fad9ace815b80e159b296fb5db148e2c9bd840ad9a5188e7a3c0778fd564b8af9ae02cd500e019a228398fb3cf + checksum: 10c0/d8b33b2390792924d0ff2b57b3098d1c6b688a788fc67e83b168928659aad7435dd2c399925a765ffa5080182634dde8f9f76c919f2c259a33af15319187f72f languageName: node linkType: hard -"@react-spring/types@npm:~10.0.1": - version: 10.0.1 - resolution: "@react-spring/types@npm:10.0.1" - checksum: 10c0/260890f9c156dc69b77c846510017156d8c0a07cce70edc7c108e57b0cf4122b26a15e724b191481a51b2c914296de9e81d56618b2c339339d4b221930691baa +"@react-spring/types@npm:~10.0.3": + version: 10.0.3 + resolution: "@react-spring/types@npm:10.0.3" + checksum: 10c0/f9bc2619dc9997fe93ebab90fd98118106e53bc45c9c279caaa7081b69d3ed0186d603d5ed445e56bb8ad0075553d15908a2a54e3a0a36ef7cd43a03c1650b02 languageName: node linkType: hard "@react-spring/web@npm:^10.0.0": - version: 10.0.1 - resolution: "@react-spring/web@npm:10.0.1" + version: 10.0.3 + resolution: "@react-spring/web@npm:10.0.3" dependencies: - "@react-spring/animated": "npm:~10.0.1" - "@react-spring/core": "npm:~10.0.1" - "@react-spring/shared": "npm:~10.0.1" - "@react-spring/types": "npm:~10.0.1" + "@react-spring/animated": "npm:~10.0.3" + "@react-spring/core": "npm:~10.0.3" + "@react-spring/shared": "npm:~10.0.3" + "@react-spring/types": "npm:~10.0.3" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/a0c788c9fd881ccb834feb22fc0694e74e59f7b76e498f0096f5b65e2c9812513955bf45ee27d7c5348f56a7bba7c5a6961d4be663728bb2172fe5aa6b6bdfc4 + checksum: 10c0/cafbf55991d68920e94419b5b081cfb0aea2ddfa193e984861ab53306365bf81152bce2861370f9b26d4027dd714e8acc0dfbd57bd2eda99e827c3212e22a51c languageName: node linkType: hard @@ -4512,7 +5033,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^5.0.1": +"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.2.0": version: 5.3.0 resolution: "@rollup/pluginutils@npm:5.3.0" dependencies: @@ -4528,22 +5049,6 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^5.1.3": - version: 5.1.3 - resolution: "@rollup/pluginutils@npm:5.1.3" - dependencies: - "@types/estree": "npm:^1.0.0" - estree-walker: "npm:^2.0.2" - picomatch: "npm:^4.0.2" - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - checksum: 10c0/ba46ad588733fb01d184ee3bc7a127d626158bc840b5874a94c129ff62689d12f16f537530709c54da6f3b71f67d705c4e09235b1dc9542e9d47ee8f2d0b8b9e - languageName: node - linkType: hard - "@rollup/rollup-android-arm-eabi@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-android-arm-eabi@npm:4.50.1" @@ -5041,39 +5546,38 @@ __metadata: linkType: hard "@testing-library/dom@npm:^10.1.0": - version: 10.4.0 - resolution: "@testing-library/dom@npm:10.4.0" + version: 10.4.1 + resolution: "@testing-library/dom@npm:10.4.1" dependencies: "@babel/code-frame": "npm:^7.10.4" "@babel/runtime": "npm:^7.12.5" "@types/aria-query": "npm:^5.0.1" aria-query: "npm:5.3.0" - chalk: "npm:^4.1.0" dom-accessibility-api: "npm:^0.5.9" lz-string: "npm:^1.5.0" + picocolors: "npm:1.1.1" pretty-format: "npm:^27.0.2" - checksum: 10c0/0352487720ecd433400671e773df0b84b8268fb3fe8e527cdfd7c11b1365b398b4e0eddba6e7e0c85e8d615f48257753283fccec41f6b986fd6c85f15eb5f84f + checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1 languageName: node linkType: hard "@testing-library/jest-dom@npm:^6.6.3": - version: 6.6.4 - resolution: "@testing-library/jest-dom@npm:6.6.4" + version: 6.9.1 + resolution: "@testing-library/jest-dom@npm:6.9.1" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" css.escape: "npm:^1.5.1" dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" picocolors: "npm:^1.1.1" redent: "npm:^3.0.0" - checksum: 10c0/cb73adf4910f654f6cc61cfb9a551efdffa04ef423bc7fbfd67a6d8aa31c6c6dc6363fe9db23a35fc7cb32ff1390e6e1c77575c2fa70d8b028a943af32bc214c + checksum: 10c0/4291ebd2f0f38d14cefac142c56c337941775a5807e2a3d6f1a14c2fbd6be76a18e498ed189e95bedc97d9e8cf1738049bc76c85b5bc5e23fae7c9e10f7b3a12 languageName: node linkType: hard "@testing-library/react@npm:^16.0.0": - version: 16.3.0 - resolution: "@testing-library/react@npm:16.3.0" + version: 16.3.1 + resolution: "@testing-library/react@npm:16.3.1" dependencies: "@babel/runtime": "npm:^7.12.5" peerDependencies: @@ -5087,7 +5591,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/3a2cb1f87c9a67e1ebbbcfd99b94b01e496fc35147be8bc5d8bf07a699c7d523a09d57ef2f7b1d91afccd1a28e21eda3b00d80187fbb51b1de01e422592d845e + checksum: 10c0/5a26ceaa4ab1d065be722d93e3b019883864ae038f9fd1c974f5b8a173f5f35a25768ecb2baa02a783299f009cbcd09fa7ee0b8b3d360d1c0f81535436358b28 languageName: node linkType: hard @@ -5100,12 +5604,12 @@ __metadata: languageName: node linkType: hard -"@tybys/wasm-util@npm:^0.9.0": - version: 0.9.0 - resolution: "@tybys/wasm-util@npm:0.9.0" +"@tybys/wasm-util@npm:^0.10.1": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/f9fde5c554455019f33af6c8215f1a1435028803dc2a2825b077d812bed4209a1a64444a4ca0ce2ea7e1175c8d88e2f9173a36a33c199e8a5c671aa31de8242d + checksum: 10c0/b255094f293794c6d2289300c5fbcafbb5532a3aed3a5ffd2f8dc1828e639b88d75f6a376dd8f94347a44813fd7a7149d8463477a9a49525c8b2dcaa38c2d1e8 languageName: node linkType: hard @@ -5280,20 +5784,20 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=13.7.0": - version: 24.1.0 - resolution: "@types/node@npm:24.1.0" + version: 25.0.3 + resolution: "@types/node@npm:25.0.3" dependencies: - undici-types: "npm:~7.8.0" - checksum: 10c0/6c4686bc144f6ce7bffd4cadc3e1196e2217c1da4c639c637213719c8a3ee58b6c596b994befcbffeacd9d9eb0c3bff6529d2bc27da5d1cb9d58b1da0056f9f4 + undici-types: "npm:~7.16.0" + checksum: 10c0/b7568f0d765d9469621615e2bb257c7fd1953d95e9acbdb58dffb6627a2c4150d405a4600aa1ad8a40182a94fe5f903cafd3c0a2f5132814debd0e3bfd61f835 languageName: node linkType: hard "@types/node@npm:^24.0.0": - version: 24.10.0 - resolution: "@types/node@npm:24.10.0" + version: 24.10.4 + resolution: "@types/node@npm:24.10.4" dependencies: undici-types: "npm:~7.16.0" - checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46 + checksum: 10c0/069639cb7233ee747df1897b5e784f6b6c5da765c96c94773c580aac888fa1a585048d2a6e95eb8302d89c7a9df75801c8b5a0b7d0221d4249059cf09a5f4228 languageName: node linkType: hard @@ -5305,43 +5809,43 @@ __metadata: linkType: hard "@types/pako@npm:^2.0.3": - version: 2.0.3 - resolution: "@types/pako@npm:2.0.3" - checksum: 10c0/45119ac3c4e8a77317c35493327039b74e333562f06ce038048228918d8ddfaa7958125aab960d1565b3861046022754c414dba1eecb210c44a32c415956bee2 + version: 2.0.4 + resolution: "@types/pako@npm:2.0.4" + checksum: 10c0/5765bf8bc7e77ee141c454118f03e544b8f6cb51eb257d82dc5830feeab8cd00818af3a1eabefdfbe8dd3ae9916ed5403937bf1031a0ee51deea27fdf4dccdfb languageName: node linkType: hard "@types/qrcode@npm:^1.5.5": - version: 1.5.5 - resolution: "@types/qrcode@npm:1.5.5" + version: 1.5.6 + resolution: "@types/qrcode@npm:1.5.6" dependencies: "@types/node": "npm:*" - checksum: 10c0/b8e6709905d1edb32dda414408acab18ac4aefcbe7bf96d9e32ba94218f45b99c8938ba7a09863ce82a67b226195099fd0f48881d16ee844899087b7f249955f + checksum: 10c0/84844ca63e5f32bc47d44dda0f8a6f7cdcc7ce44e7b24f10f19d50796f31d12c058f702a8f7d352c9e82a023a9abc36fa1ad01ddf0a209dd8ed4562ea76481fc languageName: node linkType: hard "@types/react-dom@npm:^19.0.0": - version: 19.1.6 - resolution: "@types/react-dom@npm:19.1.6" + version: 19.2.3 + resolution: "@types/react-dom@npm:19.2.3" peerDependencies: - "@types/react": ^19.0.0 - checksum: 10c0/7ba74eee2919e3f225e898b65fdaa16e54952aaf9e3472a080ddc82ca54585e46e60b3c52018d21d4b7053f09d27b8293e9f468b85f9932ff452cd290cc131e8 + "@types/react": ^19.2.0 + checksum: 10c0/b486ebe0f4e2fb35e2e108df1d8fc0927ca5d6002d5771e8a739de11239fe62d0e207c50886185253c99eb9dedfeeb956ea7429e5ba17f6693c7acb4c02f8cd1 languageName: node linkType: hard "@types/react@npm:^19.0.0": - version: 19.1.8 - resolution: "@types/react@npm:19.1.8" + version: 19.2.7 + resolution: "@types/react@npm:19.2.7" dependencies: - csstype: "npm:^3.0.2" - checksum: 10c0/4908772be6dc941df276931efeb0e781777fa76e4d5d12ff9f75eb2dcc2db3065e0100efde16fde562c5bafa310cc8f50c1ee40a22640459e066e72cd342143e + csstype: "npm:^3.2.2" + checksum: 10c0/a7b75f1f9fcb34badd6f84098be5e35a0aeca614bc91f93d2698664c0b2ba5ad128422bd470ada598238cebe4f9e604a752aead7dc6f5a92261d0c7f9b27cfd1 languageName: node linkType: hard "@types/sdp-transform@npm:^2.4.5": - version: 2.4.10 - resolution: "@types/sdp-transform@npm:2.4.10" - checksum: 10c0/3482950907edc1b841a9d550c391b89b1d56f078bed33e83fb01d2de3097c6045d4627fb50b192712ff34c71784b476dce044c4df735808a9d0bfeca1e77df7d + version: 2.15.0 + resolution: "@types/sdp-transform@npm:2.15.0" + checksum: 10c0/8740a51ef3478dcc560952d815edb650680f924e00fcb19eea74a8422640e9a1e18a63d9dda750576598cb1c15b8bb6a68bd5076a9e9ac170d828cb97fa1ff26 languageName: node linkType: hard @@ -5390,23 +5894,22 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^8.31.0": - version: 8.38.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.38.0" + version: 8.51.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.51.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.38.0" - "@typescript-eslint/type-utils": "npm:8.38.0" - "@typescript-eslint/utils": "npm:8.38.0" - "@typescript-eslint/visitor-keys": "npm:8.38.0" - graphemer: "npm:^1.4.0" + "@typescript-eslint/scope-manager": "npm:8.51.0" + "@typescript-eslint/type-utils": "npm:8.51.0" + "@typescript-eslint/utils": "npm:8.51.0" + "@typescript-eslint/visitor-keys": "npm:8.51.0" ignore: "npm:^7.0.0" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.1.0" + ts-api-utils: "npm:^2.2.0" peerDependencies: - "@typescript-eslint/parser": ^8.38.0 + "@typescript-eslint/parser": ^8.51.0 eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/199b82e9f0136baecf515df7c31bfed926a7c6d4e6298f64ee1a77c8bdd7a8cb92a2ea55a5a345c9f2948a02f7be6d72530efbe803afa1892b593fbd529d0c27 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/3140e66a0f722338d56bf3de2b7cbb9a74a812d8da90fc61975ea029f6a401252c0824063d4c4baab9827de6f0209b34f4bbdc46e3f5fefd8fa2ff4a3980406f languageName: node linkType: hard @@ -5422,31 +5925,31 @@ __metadata: linkType: hard "@typescript-eslint/parser@npm:^8.31.0": - version: 8.38.0 - resolution: "@typescript-eslint/parser@npm:8.38.0" + version: 8.51.0 + resolution: "@typescript-eslint/parser@npm:8.51.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.38.0" - "@typescript-eslint/types": "npm:8.38.0" - "@typescript-eslint/typescript-estree": "npm:8.38.0" - "@typescript-eslint/visitor-keys": "npm:8.38.0" + "@typescript-eslint/scope-manager": "npm:8.51.0" + "@typescript-eslint/types": "npm:8.51.0" + "@typescript-eslint/typescript-estree": "npm:8.51.0" + "@typescript-eslint/visitor-keys": "npm:8.51.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/5580c2a328f0c15f85e4a0961a07584013cc0aca85fe868486187f7c92e9e3f6602c6e3dab917b092b94cd492ed40827c6f5fea42730bef88eb17592c947adf4 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/b6aab1d82cc98a77aaae7637bf2934980104799793b3fd5b893065d930fe9b23cd6c2059d6f73fb454ea08f9e956e84fa940310d8435092a14be645a42062d94 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/project-service@npm:8.38.0" +"@typescript-eslint/project-service@npm:8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/project-service@npm:8.51.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.38.0" - "@typescript-eslint/types": "npm:^8.38.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.51.0" + "@typescript-eslint/types": "npm:^8.51.0" debug: "npm:^4.3.4" peerDependencies: - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/87d2f55521e289bbcdc666b1f4587ee2d43039cee927310b05abaa534b528dfb1b5565c1545bb4996d7fbdf9d5a3b0aa0e6c93a8f1289e3fcfd60d246364a884 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/c6e6efbf79e126261e1742990b0872a34bbbe9931d99f0aabd12cb70a65a361e02d626db4b632dabee2b2c26b7e5b48344fc5a796c56438ae0788535e2bbe092 languageName: node linkType: hard @@ -5470,38 +5973,38 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/scope-manager@npm:8.38.0" +"@typescript-eslint/scope-manager@npm:8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/scope-manager@npm:8.51.0" dependencies: - "@typescript-eslint/types": "npm:8.38.0" - "@typescript-eslint/visitor-keys": "npm:8.38.0" - checksum: 10c0/ceaf489ea1f005afb187932a7ee363dfe1e0f7cc3db921283991e20e4c756411a5e25afbec72edd2095d6a4384f73591f4c750cf65b5eaa650c90f64ef9fe809 + "@typescript-eslint/types": "npm:8.51.0" + "@typescript-eslint/visitor-keys": "npm:8.51.0" + checksum: 10c0/dd1e75fc13e6b1119954612d9e8ad3f2d91bc37dcde85fd00e959171aaf6c716c4c265c90c5accf24b5831bd3f48510b0775e5583085b8fa2ad5c37c8980ae1a languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.38.0, @typescript-eslint/tsconfig-utils@npm:^8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.38.0" +"@typescript-eslint/tsconfig-utils@npm:8.51.0, @typescript-eslint/tsconfig-utils@npm:^8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.51.0" peerDependencies: - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/1a90da16bf1f7cfbd0303640a8ead64a0080f2b1d5969994bdac3b80abfa1177f0c6fbf61250bae082e72cf5014308f2f5cc98edd6510202f13420a7ffd07a84 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/46cab9a5342b4a8f8a1d05aaee4236c5262a540ad0bca1f0e8dad5d63ed1e634b88ce0c82a612976dab09861e21086fc995a368df0435ac43fb960e0b9e5cde2 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/type-utils@npm:8.38.0" +"@typescript-eslint/type-utils@npm:8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/type-utils@npm:8.51.0" dependencies: - "@typescript-eslint/types": "npm:8.38.0" - "@typescript-eslint/typescript-estree": "npm:8.38.0" - "@typescript-eslint/utils": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.51.0" + "@typescript-eslint/typescript-estree": "npm:8.51.0" + "@typescript-eslint/utils": "npm:8.51.0" debug: "npm:^4.3.4" - ts-api-utils: "npm:^2.1.0" + ts-api-utils: "npm:^2.2.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/27795c4bd0be395dda3424e57d746639c579b7522af1c17731b915298a6378fd78869e8e141526064b6047db2c86ba06444469ace19c98cda5779d06f4abd37c + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/7c17214e54bc3a4fe4551d9251ffbac52e84ca46eeae840c0f981994b7cbcc837ef32a2b6d510b02d958a8f568df355e724d9c6938a206716271a1b0c00801b7 languageName: node linkType: hard @@ -5519,10 +6022,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.38.0, @typescript-eslint/types@npm:^8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/types@npm:8.38.0" - checksum: 10c0/f0ac0060c98c0f3d1871f107177b6ae25a0f1846ca8bd8cfc7e1f1dd0ddce293cd8ac4a5764d6a767de3503d5d01defcd68c758cb7ba6de52f82b209a918d0d2 +"@typescript-eslint/types@npm:8.51.0, @typescript-eslint/types@npm:^8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/types@npm:8.51.0" + checksum: 10c0/eb3473d0bb71eb886438f35887b620ffadae7853b281752a40c73158aee644d136adeb82549be7d7c30f346fe888b2e979dff7e30e67b35377e8281018034529 languageName: node linkType: hard @@ -5562,23 +6065,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.38.0" +"@typescript-eslint/typescript-estree@npm:8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.51.0" dependencies: - "@typescript-eslint/project-service": "npm:8.38.0" - "@typescript-eslint/tsconfig-utils": "npm:8.38.0" - "@typescript-eslint/types": "npm:8.38.0" - "@typescript-eslint/visitor-keys": "npm:8.38.0" + "@typescript-eslint/project-service": "npm:8.51.0" + "@typescript-eslint/tsconfig-utils": "npm:8.51.0" + "@typescript-eslint/types": "npm:8.51.0" + "@typescript-eslint/visitor-keys": "npm:8.51.0" debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.2" - is-glob: "npm:^4.0.3" minimatch: "npm:^9.0.4" semver: "npm:^7.6.0" - ts-api-utils: "npm:^2.1.0" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.2.0" peerDependencies: - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/00a00f6549877f4ae5c2847fa5ac52bf42cbd59a87533856c359e2746e448ed150b27a6137c92fd50c06e6a4b39e386d6b738fac97d80d05596e81ce55933230 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/5386acc67298a6757681b6264c29a6b9304be7a188f11498bbaa82bb0a3095fd79394ad80d6520bdff3fa3093199f9a438246604ee3281b76f7ed574b7516854 languageName: node linkType: hard @@ -5600,18 +6102,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/utils@npm:8.38.0" +"@typescript-eslint/utils@npm:8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/utils@npm:8.51.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.7.0" - "@typescript-eslint/scope-manager": "npm:8.38.0" - "@typescript-eslint/types": "npm:8.38.0" - "@typescript-eslint/typescript-estree": "npm:8.38.0" + "@typescript-eslint/scope-manager": "npm:8.51.0" + "@typescript-eslint/types": "npm:8.51.0" + "@typescript-eslint/typescript-estree": "npm:8.51.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/e97a45bf44f315f9ed8c2988429e18c88e3369c9ee3227ee86446d2d49f7325abebbbc9ce801e178f676baa986d3e1fd4b5391f1640c6eb8944c123423ae43bb + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/ffb8237cfb33a1998ae2812b136d42fb65e7497f185d46097d19e43112e41b3ef59f901ba679c2e5372ad3007026f6e5add3a3de0f2e75ce6896918713fa38a8 languageName: node linkType: hard @@ -5650,13 +6152,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.38.0": - version: 8.38.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.38.0" +"@typescript-eslint/visitor-keys@npm:8.51.0": + version: 8.51.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.51.0" dependencies: - "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.51.0" eslint-visitor-keys: "npm:^4.2.1" - checksum: 10c0/071a756e383f41a6c9e51d78c8c64bd41cd5af68b0faef5fbaec4fa5dbd65ec9e4cd610c2e2cdbe9e2facc362995f202850622b78e821609a277b5b601a1d4ec + checksum: 10c0/fce5603961cf336e71095f7599157de65e3182f61cbd6cab33a43551ee91485b4e9bf6cacc1b275cf6f3503b92f8568fe2267a45c82e60e386ee73db727a26ca languageName: node linkType: hard @@ -6223,21 +6725,20 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:^10.4.21": - version: 10.4.21 - resolution: "autoprefixer@npm:10.4.21" +"autoprefixer@npm:^10.4.23": + version: 10.4.23 + resolution: "autoprefixer@npm:10.4.23" dependencies: - browserslist: "npm:^4.24.4" - caniuse-lite: "npm:^1.0.30001702" - fraction.js: "npm:^4.3.7" - normalize-range: "npm:^0.1.2" + browserslist: "npm:^4.28.1" + caniuse-lite: "npm:^1.0.30001760" + fraction.js: "npm:^5.3.4" picocolors: "npm:^1.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.1.0 bin: autoprefixer: bin/autoprefixer - checksum: 10c0/de5b71d26d0baff4bbfb3d59f7cf7114a6030c9eeb66167acf49a32c5b61c68e308f1e0f869d92334436a221035d08b51cd1b2f2c4689b8d955149423c16d4d4 + checksum: 10c0/3765c5d0fa3e95fb2ebe9d5a6d4da0156f5d346c7ec9ac0fbf5c97c8139d0ca1e8743bf5dc1b4aa954467be6929fddf8498a3b6202d468d70b5f359f3b6af90f languageName: node linkType: hard @@ -6345,6 +6846,15 @@ __metadata: languageName: node linkType: hard +"baseline-browser-mapping@npm:^2.9.0": + version: 2.9.11 + resolution: "baseline-browser-mapping@npm:2.9.11" + bin: + baseline-browser-mapping: dist/cli.js + checksum: 10c0/eba49fcc1b33ab994aeeb73a4848f2670e06a0886dd5b903689ae6f60d47e7f1bea9262dbb2548c48179e858f7eda2b82ddf941ae783b862f4dcc51085a246f2 + languageName: node + linkType: hard + "before-after-hook@npm:^2.2.0": version: 2.2.3 resolution: "before-after-hook@npm:2.2.3" @@ -6560,7 +7070,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.24.0, browserslist@npm:^4.24.3, browserslist@npm:^4.24.4": +"browserslist@npm:^4.24.0, browserslist@npm:^4.24.3": version: 4.24.4 resolution: "browserslist@npm:4.24.4" dependencies: @@ -6574,7 +7084,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.25.0, browserslist@npm:^4.25.1": +"browserslist@npm:^4.25.1": version: 4.25.1 resolution: "browserslist@npm:4.25.1" dependencies: @@ -6588,6 +7098,21 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.28.1": + version: 4.28.1 + resolution: "browserslist@npm:4.28.1" + dependencies: + baseline-browser-mapping: "npm:^2.9.0" + caniuse-lite: "npm:^1.0.30001759" + electron-to-chromium: "npm:^1.5.263" + node-releases: "npm:^2.0.27" + update-browserslist-db: "npm:^1.2.0" + bin: + browserslist: cli.js + checksum: 10c0/545a5fa9d7234e3777a7177ec1e9134bb2ba60a69e6b95683f6982b1473aad347c77c1264ccf2ac5dea609a9731fbfbda6b85782bdca70f80f86e28a402504bd + languageName: node + linkType: hard + "bs58@npm:^6.0.0": version: 6.0.0 resolution: "bs58@npm:6.0.0" @@ -6762,13 +7287,20 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001688, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001726": +"caniuse-lite@npm:^1.0.30001688, caniuse-lite@npm:^1.0.30001726": version: 1.0.30001757 resolution: "caniuse-lite@npm:1.0.30001757" checksum: 10c0/3ccb71fa2bf1f8c96ff1bf9b918b08806fed33307e20a3ce3259155fda131eaf96cfcd88d3d309c8fd7f8285cc71d89a3b93648a1c04814da31c301f98508d42 languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001759, caniuse-lite@npm:^1.0.30001760": + version: 1.0.30001762 + resolution: "caniuse-lite@npm:1.0.30001762" + checksum: 10c0/93707eac5b0240af3f2ce6e2d7ab504a6fefcf9c2f9cd8fb9d488e496a333c61e557dab0472c1b00c17bc386a5dbb792aa4c778cda2d768e17f986617d7aec53 + languageName: node + linkType: hard + "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -6789,7 +7321,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:~4.1.0": +"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:~4.1.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -7256,16 +7788,16 @@ __metadata: languageName: node linkType: hard -"css-has-pseudo@npm:^7.0.2": - version: 7.0.2 - resolution: "css-has-pseudo@npm:7.0.2" +"css-has-pseudo@npm:^7.0.3": + version: 7.0.3 + resolution: "css-has-pseudo@npm:7.0.3" dependencies: "@csstools/selector-specificity": "npm:^5.0.0" postcss-selector-parser: "npm:^7.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/456e9ce1eec8a535683c329956acfe53ce5a208345d7f2fcbe662626be8b3c98681e9041d7f4980316714397b0c1c3defde25653d629c396df17803d599c4edf + checksum: 10c0/c89f68e17bed229e9a3e98da5032e1360c83d45d974bc3fb8d6b5358399bca80cce7929e4a621a516a75536edb78678dc486eb41841eeed28cca79e3be4bdc27 languageName: node linkType: hard @@ -7318,10 +7850,10 @@ __metadata: languageName: node linkType: hard -"cssdb@npm:^8.3.0": - version: 8.3.0 - resolution: "cssdb@npm:8.3.0" - checksum: 10c0/56d13cbddd90e63f45f24f71f35314f9718b72760acdf15367e33014eb45df775ae97ec05c08afaa6b4b147c757e9554c1bf39ddcdaeeb26b6c2adfeee503ae7 +"cssdb@npm:^8.6.0": + version: 8.6.0 + resolution: "cssdb@npm:8.6.0" + checksum: 10c0/4bb7b77ba24902e8d481e9514ec0be56e205186a2b7d9f5027fedfe718952c559c62acfd2859f92869f8090da7c2170f83d68170db5058a6ba8d9d5e8ded3b3e languageName: node linkType: hard @@ -7344,10 +7876,10 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.2": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 10c0/80c089d6f7e0c5b2bd83cf0539ab41474198579584fa10d86d0cafe0642202343cbc119e076a0b1aece191989477081415d66c9fefbf3c957fc2fc4b7009f248 +"csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce languageName: node linkType: hard @@ -7754,6 +8286,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.263": + version: 1.5.267 + resolution: "electron-to-chromium@npm:1.5.267" + checksum: 10c0/0732bdb891b657f2e43266a3db8cf86fff6cecdcc8d693a92beff214e136cb5c2ee7dc5945ed75fa1db16e16bad0c38695527a020d15f39e79084e0b2e447621 + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.5.73": version: 1.5.109 resolution: "electron-to-chromium@npm:1.5.109" @@ -7772,16 +8311,16 @@ __metadata: "@codecov/vite-plugin": "npm:^1.3.0" "@fontsource/inconsolata": "npm:^5.1.0" "@fontsource/inter": "npm:^5.1.0" - "@formatjs/intl-durationformat": "npm:^0.7.0" + "@formatjs/intl-durationformat": "npm:^0.9.0" "@formatjs/intl-segmenter": "npm:^11.7.3" "@livekit/components-core": "npm:^0.12.0" "@livekit/components-react": "npm:^2.0.0" "@livekit/protocol": "npm:^1.42.2" - "@livekit/track-processors": "npm:^0.5.5" + "@livekit/track-processors": "npm:^0.7.0" "@mediapipe/tasks-vision": "npm:^0.10.18" "@opentelemetry/api": "npm:^1.4.0" "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:^0.203.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:^0.208.0" "@opentelemetry/resources": "npm:^2.0.0" "@opentelemetry/sdk-trace-base": "npm:^2.0.0" "@opentelemetry/sdk-trace-web": "npm:^2.0.0" @@ -8283,6 +8822,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.27.0": + version: 0.27.2 + resolution: "esbuild@npm:0.27.2" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.2" + "@esbuild/android-arm": "npm:0.27.2" + "@esbuild/android-arm64": "npm:0.27.2" + "@esbuild/android-x64": "npm:0.27.2" + "@esbuild/darwin-arm64": "npm:0.27.2" + "@esbuild/darwin-x64": "npm:0.27.2" + "@esbuild/freebsd-arm64": "npm:0.27.2" + "@esbuild/freebsd-x64": "npm:0.27.2" + "@esbuild/linux-arm": "npm:0.27.2" + "@esbuild/linux-arm64": "npm:0.27.2" + "@esbuild/linux-ia32": "npm:0.27.2" + "@esbuild/linux-loong64": "npm:0.27.2" + "@esbuild/linux-mips64el": "npm:0.27.2" + "@esbuild/linux-ppc64": "npm:0.27.2" + "@esbuild/linux-riscv64": "npm:0.27.2" + "@esbuild/linux-s390x": "npm:0.27.2" + "@esbuild/linux-x64": "npm:0.27.2" + "@esbuild/netbsd-arm64": "npm:0.27.2" + "@esbuild/netbsd-x64": "npm:0.27.2" + "@esbuild/openbsd-arm64": "npm:0.27.2" + "@esbuild/openbsd-x64": "npm:0.27.2" + "@esbuild/openharmony-arm64": "npm:0.27.2" + "@esbuild/sunos-x64": "npm:0.27.2" + "@esbuild/win32-arm64": "npm:0.27.2" + "@esbuild/win32-ia32": "npm:0.27.2" + "@esbuild/win32-x64": "npm:0.27.2" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/cf83f626f55500f521d5fe7f4bc5871bec240d3deb2a01fbd379edc43b3664d1167428738a5aad8794b35d1cca985c44c375b1cd38a2ca613c77ced2c83aafcd + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -8943,21 +9571,21 @@ __metadata: languageName: node linkType: hard -"formatly@npm:^0.2.4": - version: 0.2.4 - resolution: "formatly@npm:0.2.4" +"formatly@npm:^0.3.0": + version: 0.3.0 + resolution: "formatly@npm:0.3.0" dependencies: fd-package-json: "npm:^2.0.0" bin: formatly: bin/index.mjs - checksum: 10c0/43c6272a12199bc6319e7ef7043f209e7005fc35bc1b15e96ef16ad46a12fddc2b7c179fe8ade174c728e8454e3ebdc8428867cee78b082d18a91dae72866336 + checksum: 10c0/ef9dbd3cdaee649e9604ea060d8d62d8131eb81117634336592ee2193fc7c98a3f1f1b5d09a045dbd36287ba88edf868ef179d39fbda2f34fbe2be70c42dd014 languageName: node linkType: hard -"fraction.js@npm:^4.3.7": - version: 4.3.7 - resolution: "fraction.js@npm:4.3.7" - checksum: 10c0/df291391beea9ab4c263487ffd9d17fed162dbb736982dee1379b2a8cc94e4e24e46ed508c6d278aded9080ba51872f1bc5f3a5fd8d7c74e5f105b508ac28711 +"fraction.js@npm:^5.3.4": + version: 5.3.4 + resolution: "fraction.js@npm:5.3.4" + checksum: 10c0/f90079fe9bfc665e0a07079938e8ff71115bce9462f17b32fc283f163b0540ec34dc33df8ed41bb56f028316b04361b9a9995b9ee9258617f8338e0b05c5f95a languageName: node linkType: hard @@ -10240,12 +10868,12 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^2.4.2": - version: 2.4.2 - resolution: "jiti@npm:2.4.2" +"jiti@npm:^2.6.0": + version: 2.6.1 + resolution: "jiti@npm:2.6.1" bin: jiti: lib/jiti-cli.mjs - checksum: 10c0/4ceac133a08c8faff7eac84aabb917e85e8257f5ad659e843004ce76e981c457c390a220881748ac67ba1b940b9b729b30fb85cbaf6e7989f04b6002c94da331 + checksum: 10c0/79b2e96a8e623f66c1b703b98ec1b8be4500e1d217e09b09e343471bbb9c105381b83edbb979d01cef18318cc45ce6e153571b6c83122170eefa531c64b6789b languageName: node linkType: hard @@ -10288,6 +10916,17 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7 + languageName: node + linkType: hard + "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -10464,29 +11103,28 @@ __metadata: linkType: hard "knip@npm:^5.27.2": - version: 5.62.0 - resolution: "knip@npm:5.62.0" + version: 5.79.0 + resolution: "knip@npm:5.79.0" dependencies: "@nodelib/fs.walk": "npm:^1.2.3" fast-glob: "npm:^3.3.3" - formatly: "npm:^0.2.4" - jiti: "npm:^2.4.2" - js-yaml: "npm:^4.1.0" + formatly: "npm:^0.3.0" + jiti: "npm:^2.6.0" + js-yaml: "npm:^4.1.1" minimist: "npm:^1.2.8" - oxc-resolver: "npm:^11.1.0" + oxc-resolver: "npm:^11.15.0" picocolors: "npm:^1.1.1" picomatch: "npm:^4.0.1" - smol-toml: "npm:^1.3.4" - strip-json-comments: "npm:5.0.2" - zod: "npm:^3.22.4" - zod-validation-error: "npm:^3.0.3" + smol-toml: "npm:^1.5.2" + strip-json-comments: "npm:5.0.3" + zod: "npm:^4.1.11" peerDependencies: "@types/node": ">=18" - typescript: ">=5.0.4" + typescript: ">=5.0.4 <7" bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10c0/ffc6c123d132bb423936859c3ae5cb85154cfc862985f74637bc54a4920ef34c1f19d41020f7af25100ea8e7ae61f1f5279af8066043434444bcba82b8dc1611 + checksum: 10c0/dc3599247763912c0602621b83d125cba4e111d85ec5f01f9b65808a0091a60d7be85ed6cecc93d0afb39b895127231f7a68dd4c4bb7e210dd727b6ef9c1571d languageName: node linkType: hard @@ -10576,9 +11214,9 @@ __metadata: linkType: hard "lodash-es@npm:^4.17.21": - version: 4.17.21 - resolution: "lodash-es@npm:4.17.21" - checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2 + version: 4.17.22 + resolution: "lodash-es@npm:4.17.22" + checksum: 10c0/5f28a262183cca43e08c580622557f393cb889386df2d8adf7c852bfdff7a84c5e629df5aa6c5c6274e83b38172f239d3e4e72e1ad27352d9ae9766627338089 languageName: node linkType: hard @@ -10596,13 +11234,6 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.21": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c - languageName: node - linkType: hard - "loglevel@npm:1.9.1": version: 1.9.1 resolution: "loglevel@npm:1.9.1" @@ -11139,6 +11770,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.27": + version: 2.0.27 + resolution: "node-releases@npm:2.0.27" + checksum: 10c0/f1e6583b7833ea81880627748d28a3a7ff5703d5409328c216ae57befbced10ce2c991bea86434e8ec39003bd017f70481e2e5f8c1f7e0a7663241f81d6e00e2 + languageName: node + linkType: hard + "node-stdlib-browser@npm:^1.3.1": version: 1.3.1 resolution: "node-stdlib-browser@npm:1.3.1" @@ -11204,13 +11842,6 @@ __metadata: languageName: node linkType: hard -"normalize-range@npm:^0.1.2": - version: 0.1.2 - resolution: "normalize-range@npm:0.1.2" - checksum: 10c0/bf39b73a63e0a42ad1a48c2bd1bda5a07ede64a7e2567307a407674e595bcff0fa0d57e8e5f1e7fa5e91000797c7615e13613227aaaa4d6d6e87f5bd5cc95de6 - languageName: node - linkType: hard - "normalize.css@npm:^8.0.1": version: 8.0.1 resolution: "normalize.css@npm:8.0.1" @@ -11403,24 +12034,35 @@ __metadata: languageName: node linkType: hard -"oxc-resolver@npm:^11.1.0": - version: 11.3.0 - resolution: "oxc-resolver@npm:11.3.0" +"oxc-resolver@npm:^11.15.0": + version: 11.16.2 + resolution: "oxc-resolver@npm:11.16.2" dependencies: - "@oxc-resolver/binding-darwin-arm64": "npm:11.3.0" - "@oxc-resolver/binding-darwin-x64": "npm:11.3.0" - "@oxc-resolver/binding-freebsd-x64": "npm:11.3.0" - "@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.3.0" - "@oxc-resolver/binding-linux-arm64-gnu": "npm:11.3.0" - "@oxc-resolver/binding-linux-arm64-musl": "npm:11.3.0" - "@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.3.0" - "@oxc-resolver/binding-linux-s390x-gnu": "npm:11.3.0" - "@oxc-resolver/binding-linux-x64-gnu": "npm:11.3.0" - "@oxc-resolver/binding-linux-x64-musl": "npm:11.3.0" - "@oxc-resolver/binding-wasm32-wasi": "npm:11.3.0" - "@oxc-resolver/binding-win32-arm64-msvc": "npm:11.3.0" - "@oxc-resolver/binding-win32-x64-msvc": "npm:11.3.0" + "@oxc-resolver/binding-android-arm-eabi": "npm:11.16.2" + "@oxc-resolver/binding-android-arm64": "npm:11.16.2" + "@oxc-resolver/binding-darwin-arm64": "npm:11.16.2" + "@oxc-resolver/binding-darwin-x64": "npm:11.16.2" + "@oxc-resolver/binding-freebsd-x64": "npm:11.16.2" + "@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.16.2" + "@oxc-resolver/binding-linux-arm-musleabihf": "npm:11.16.2" + "@oxc-resolver/binding-linux-arm64-gnu": "npm:11.16.2" + "@oxc-resolver/binding-linux-arm64-musl": "npm:11.16.2" + "@oxc-resolver/binding-linux-ppc64-gnu": "npm:11.16.2" + "@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.16.2" + "@oxc-resolver/binding-linux-riscv64-musl": "npm:11.16.2" + "@oxc-resolver/binding-linux-s390x-gnu": "npm:11.16.2" + "@oxc-resolver/binding-linux-x64-gnu": "npm:11.16.2" + "@oxc-resolver/binding-linux-x64-musl": "npm:11.16.2" + "@oxc-resolver/binding-openharmony-arm64": "npm:11.16.2" + "@oxc-resolver/binding-wasm32-wasi": "npm:11.16.2" + "@oxc-resolver/binding-win32-arm64-msvc": "npm:11.16.2" + "@oxc-resolver/binding-win32-ia32-msvc": "npm:11.16.2" + "@oxc-resolver/binding-win32-x64-msvc": "npm:11.16.2" dependenciesMeta: + "@oxc-resolver/binding-android-arm-eabi": + optional: true + "@oxc-resolver/binding-android-arm64": + optional: true "@oxc-resolver/binding-darwin-arm64": optional: true "@oxc-resolver/binding-darwin-x64": @@ -11429,25 +12071,35 @@ __metadata: optional: true "@oxc-resolver/binding-linux-arm-gnueabihf": optional: true + "@oxc-resolver/binding-linux-arm-musleabihf": + optional: true "@oxc-resolver/binding-linux-arm64-gnu": optional: true "@oxc-resolver/binding-linux-arm64-musl": optional: true + "@oxc-resolver/binding-linux-ppc64-gnu": + optional: true "@oxc-resolver/binding-linux-riscv64-gnu": optional: true + "@oxc-resolver/binding-linux-riscv64-musl": + optional: true "@oxc-resolver/binding-linux-s390x-gnu": optional: true "@oxc-resolver/binding-linux-x64-gnu": optional: true "@oxc-resolver/binding-linux-x64-musl": optional: true + "@oxc-resolver/binding-openharmony-arm64": + optional: true "@oxc-resolver/binding-wasm32-wasi": optional: true "@oxc-resolver/binding-win32-arm64-msvc": optional: true + "@oxc-resolver/binding-win32-ia32-msvc": + optional: true "@oxc-resolver/binding-win32-x64-msvc": optional: true - checksum: 10c0/9437976bd39125538e031b89e75e29e7340b844da16afa1088d2c2a21770051a81f583dd77823894d7065ee2dc6431d8c5f158ac227035d33b430b78efa358dc + checksum: 10c0/b20a0fea18fdf31dbaee51354ce7b987ba8f3e780c6c1de9034628033a69d0b3085f9596d9925797d9340bdf4b98cd72a258b0728d0d5e5de2b1748154921b42 languageName: node linkType: hard @@ -11707,7 +12359,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": +"picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -11811,18 +12463,18 @@ __metadata: languageName: node linkType: hard -"postcss-color-functional-notation@npm:^7.0.10": - version: 7.0.10 - resolution: "postcss-color-functional-notation@npm:7.0.10" +"postcss-color-functional-notation@npm:^7.0.12": + version: 7.0.12 + resolution: "postcss-color-functional-notation@npm:7.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/62ee77ef220488cfb4a1c5af4f5203a0c2951c8a0613088ffc946130d48b63ca28ab67b18ed380a288a7ce51c2360a75d8d08d2db389e48f4ebb78a3e52d15b6 + checksum: 10c0/dc80ba1a956ae9b396596bda72d9bdb92de96874378a38ba4e2177ffa35339dc76d894920bb013b6f10c9b75cfb41778e09956a438c2e9ea41b684f766c55f4a languageName: node linkType: hard @@ -11904,16 +12556,16 @@ __metadata: languageName: node linkType: hard -"postcss-double-position-gradients@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-double-position-gradients@npm:6.0.2" +"postcss-double-position-gradients@npm:^6.0.4": + version: 6.0.4 + resolution: "postcss-double-position-gradients@npm:6.0.4" dependencies: - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/7b4759813f99039c6a7c8e70b46ff4c34c27e723a9ff7f0e1044e293d568357e1d39233f94b1bf3b2768b1207348138faea0781086a66b7b8e39e780657da523 + checksum: 10c0/6dbbe7a3855e84a9319df434e210225f6dfa7262e5959611355f1769c2c9d30d37a19737712f20eac6354876fff4ba556d8d0b12a90c78d8ab97c9a8da534a7c languageName: node linkType: hard @@ -11969,18 +12621,18 @@ __metadata: languageName: node linkType: hard -"postcss-lab-function@npm:^7.0.10": - version: 7.0.10 - resolution: "postcss-lab-function@npm:7.0.10" +"postcss-lab-function@npm:^7.0.12": + version: 7.0.12 + resolution: "postcss-lab-function@npm:7.0.12" dependencies: - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/3e235b52f6c119937a0b41aa351f5f9ef6e17bf1b868e7068c9a04f3d31c247d0296c862388febb7fec5102d81413ccade8a4788904289afd34aa072de71390b + checksum: 10c0/de39b59da3b97c18d055d81fba68993e93253184ed76f103c888273584f868c551d047814dd54445980a1bdc5987e8f8af141383d84ecc641e5a6ee7bd901095 languageName: node linkType: hard @@ -12049,23 +12701,26 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.0.0": - version: 10.2.4 - resolution: "postcss-preset-env@npm:10.2.4" + version: 10.6.0 + resolution: "postcss-preset-env@npm:10.6.0" dependencies: + "@csstools/postcss-alpha-function": "npm:^1.0.1" "@csstools/postcss-cascade-layers": "npm:^5.0.2" - "@csstools/postcss-color-function": "npm:^4.0.10" - "@csstools/postcss-color-mix-function": "npm:^3.0.10" - "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^1.0.0" - "@csstools/postcss-content-alt-text": "npm:^2.0.6" + "@csstools/postcss-color-function": "npm:^4.0.12" + "@csstools/postcss-color-function-display-p3-linear": "npm:^1.0.1" + "@csstools/postcss-color-mix-function": "npm:^3.0.12" + "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^1.0.2" + "@csstools/postcss-content-alt-text": "npm:^2.0.8" + "@csstools/postcss-contrast-color-function": "npm:^2.0.12" "@csstools/postcss-exponential-functions": "npm:^2.0.9" "@csstools/postcss-font-format-keywords": "npm:^4.0.0" - "@csstools/postcss-gamut-mapping": "npm:^2.0.10" - "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.10" - "@csstools/postcss-hwb-function": "npm:^4.0.10" - "@csstools/postcss-ic-unit": "npm:^4.0.2" + "@csstools/postcss-gamut-mapping": "npm:^2.0.11" + "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.12" + "@csstools/postcss-hwb-function": "npm:^4.0.12" + "@csstools/postcss-ic-unit": "npm:^4.0.4" "@csstools/postcss-initial": "npm:^2.0.1" "@csstools/postcss-is-pseudo-class": "npm:^5.0.3" - "@csstools/postcss-light-dark-function": "npm:^2.0.9" + "@csstools/postcss-light-dark-function": "npm:^2.0.11" "@csstools/postcss-logical-float-and-clear": "npm:^3.0.0" "@csstools/postcss-logical-overflow": "npm:^2.0.0" "@csstools/postcss-logical-overscroll-behavior": "npm:^2.0.0" @@ -12075,38 +12730,42 @@ __metadata: "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.5" "@csstools/postcss-nested-calc": "npm:^4.0.0" "@csstools/postcss-normalize-display-values": "npm:^4.0.0" - "@csstools/postcss-oklab-function": "npm:^4.0.10" - "@csstools/postcss-progressive-custom-properties": "npm:^4.1.0" + "@csstools/postcss-oklab-function": "npm:^4.0.12" + "@csstools/postcss-position-area-property": "npm:^1.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" + "@csstools/postcss-property-rule-prelude-list": "npm:^1.0.0" "@csstools/postcss-random-function": "npm:^2.0.1" - "@csstools/postcss-relative-color-syntax": "npm:^3.0.10" + "@csstools/postcss-relative-color-syntax": "npm:^3.0.12" "@csstools/postcss-scope-pseudo-class": "npm:^4.0.1" "@csstools/postcss-sign-functions": "npm:^1.1.4" "@csstools/postcss-stepped-value-functions": "npm:^4.0.9" - "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.2" + "@csstools/postcss-syntax-descriptor-syntax-production": "npm:^1.0.1" + "@csstools/postcss-system-ui-font-family": "npm:^1.0.0" + "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.3" "@csstools/postcss-trigonometric-functions": "npm:^4.0.9" "@csstools/postcss-unset-value": "npm:^4.0.0" - autoprefixer: "npm:^10.4.21" - browserslist: "npm:^4.25.0" + autoprefixer: "npm:^10.4.23" + browserslist: "npm:^4.28.1" css-blank-pseudo: "npm:^7.0.1" - css-has-pseudo: "npm:^7.0.2" + css-has-pseudo: "npm:^7.0.3" css-prefers-color-scheme: "npm:^10.0.0" - cssdb: "npm:^8.3.0" + cssdb: "npm:^8.6.0" postcss-attribute-case-insensitive: "npm:^7.0.1" postcss-clamp: "npm:^4.1.0" - postcss-color-functional-notation: "npm:^7.0.10" + postcss-color-functional-notation: "npm:^7.0.12" postcss-color-hex-alpha: "npm:^10.0.0" postcss-color-rebeccapurple: "npm:^10.0.0" postcss-custom-media: "npm:^11.0.6" postcss-custom-properties: "npm:^14.0.6" postcss-custom-selectors: "npm:^8.0.5" postcss-dir-pseudo-class: "npm:^9.0.1" - postcss-double-position-gradients: "npm:^6.0.2" + postcss-double-position-gradients: "npm:^6.0.4" postcss-focus-visible: "npm:^10.0.1" postcss-focus-within: "npm:^9.0.1" postcss-font-variant: "npm:^5.0.0" postcss-gap-properties: "npm:^6.0.0" postcss-image-set-function: "npm:^7.0.0" - postcss-lab-function: "npm:^7.0.10" + postcss-lab-function: "npm:^7.0.12" postcss-logical: "npm:^8.1.0" postcss-nesting: "npm:^13.0.2" postcss-opacity-percentage: "npm:^3.0.0" @@ -12118,7 +12777,7 @@ __metadata: postcss-selector-not: "npm:^8.0.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/d7f8494d355567dc4ea66fe765c86ba9b1e9ce5061ada5c80c51fdf6c98b004b0b7ef17b5f64d197e1bec2e22ef4b6c613b998e1c1bcad0b53f0a3e303ded2fe + checksum: 10c0/61162c9d675004db842d58829605c3c9ee81ed1a15684793a419b94c2c28e3be2ff9a7373f0996a1a255caf208d8f3d5dd907e61af1bbb0c7634e3215e87fc56 languageName: node linkType: hard @@ -12207,11 +12866,11 @@ __metadata: linkType: hard "prettier@npm:^3.0.0": - version: 3.6.2 - resolution: "prettier@npm:3.6.2" + version: 3.7.4 + resolution: "prettier@npm:3.7.4" bin: prettier: bin/prettier.cjs - checksum: 10c0/488cb2f2b99ec13da1e50074912870217c11edaddedeadc649b1244c749d15ba94e846423d062e2c4c9ae683e2d65f754de28889ba06e697ac4f988d44f45812 + checksum: 10c0/9675d2cd08eacb1faf1d1a2dbfe24bfab6a912b059fc9defdb380a408893d88213e794a40a2700bd29b140eb3172e0b07c852853f6e22f16f3374659a1a13389 languageName: node linkType: hard @@ -12414,24 +13073,24 @@ __metadata: linkType: hard "react-dom@npm:19": - version: 19.1.0 - resolution: "react-dom@npm:19.1.0" + version: 19.2.3 + resolution: "react-dom@npm:19.2.3" dependencies: - scheduler: "npm:^0.26.0" + scheduler: "npm:^0.27.0" peerDependencies: - react: ^19.1.0 - checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc + react: ^19.2.3 + checksum: 10c0/dc43f7ede06f46f3acc16ee83107c925530de9b91d1d0b3824583814746ff4c498ea64fd65cd83aba363205268adff52e2827c582634ae7b15069deaeabc4892 languageName: node linkType: hard "react-i18next@npm:^15.0.0": - version: 15.6.1 - resolution: "react-i18next@npm:15.6.1" + version: 15.7.4 + resolution: "react-i18next@npm:15.7.4" dependencies: "@babel/runtime": "npm:^7.27.6" html-parse-stringify: "npm:^3.0.1" peerDependencies: - i18next: ">= 23.2.3" + i18next: ">= 23.4.0" react: ">= 16.8.0" typescript: ^5 peerDependenciesMeta: @@ -12441,7 +13100,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10c0/10cd131005a70a493e307f4f710ea9921d5a955a5281064cad79162e5051e2bcfc119f50c7bee0fad06e33a1795308a006089d5b67969d52b66afbe149514305 + checksum: 10c0/643c5d3ced4b44084c871a55e876159561c14f378f90bf53286c1291082703e293573da18ad692b43b357b60d2f7251bc417feb0b522de8cec5c414e5ebdf6c1 languageName: node linkType: hard @@ -12521,20 +13180,20 @@ __metadata: linkType: hard "react-router-dom@npm:^7.0.0": - version: 7.7.1 - resolution: "react-router-dom@npm:7.7.1" + version: 7.11.0 + resolution: "react-router-dom@npm:7.11.0" dependencies: - react-router: "npm:7.7.1" + react-router: "npm:7.11.0" peerDependencies: react: ">=18" react-dom: ">=18" - checksum: 10c0/292455db6991d8559a9e94857440393d9fd011471ff231f8c3a40be6749f74347f20c183a2e9b52830ec54d2bf3b2e1310c006e709e66b27206b0c36ab54def6 + checksum: 10c0/0e8061fe0ef7915cc411dd92f5f41109f6343b6abef36571b08ff231365bf61f52364bea128d1c964e9b8eb19426c9bd21923df0b3e1bb993d21bd2b7440fb49 languageName: node linkType: hard -"react-router@npm:7.7.1": - version: 7.7.1 - resolution: "react-router@npm:7.7.1" +"react-router@npm:7.11.0": + version: 7.11.0 + resolution: "react-router@npm:7.11.0" dependencies: cookie: "npm:^1.0.1" set-cookie-parser: "npm:^2.6.0" @@ -12544,7 +13203,7 @@ __metadata: peerDependenciesMeta: react-dom: optional: true - checksum: 10c0/e55fe74a2947939526c79e496ab1fc501fd8e89a191a20157d94cfe712d4d9d84f68627811cf1d477a36b98250fcad958bf1237fc41ff0a8b2de00ddc8c53e3b + checksum: 10c0/eb3693d63d1c52221a3449de5db170e2fa9e00536b011998b17f8a277f8b5e89b752d104dbbeb4ee3d474f8e4570167db00293b4510f63277e5e6658c5dab22b languageName: node linkType: hard @@ -12578,9 +13237,9 @@ __metadata: linkType: hard "react@npm:19": - version: 19.1.0 - resolution: "react@npm:19.1.0" - checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 + version: 19.2.3 + resolution: "react@npm:19.2.3" + checksum: 10c0/094220b3ba3a76c1b668f972ace1dd15509b157aead1b40391d1c8e657e720c201d9719537375eff08f5e0514748c0319063392a6f000e31303aafc4471f1436 languageName: node linkType: hard @@ -13187,8 +13846,8 @@ __metadata: linkType: hard "sass@npm:^1.42.1": - version: 1.89.2 - resolution: "sass@npm:1.89.2" + version: 1.97.1 + resolution: "sass@npm:1.97.1" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -13199,7 +13858,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/752ccc7581b0c6395f63918116c20924e99943a86d79e94f5c4a0d41b1e981fe1f0ecd1ee82fff21496f81dbc91f68fb35a498166562ec8ec53e7aad7c3dbd9d + checksum: 10c0/c389d5d6405869b49fa2291e8328500fe7936f3b72136bc2c338bee6e7fec936bb9a48d77a1310dea66aa4669ba74ae6b82a112eb32521b9b36d740138a39ea0 languageName: node linkType: hard @@ -13212,10 +13871,10 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.26.0": - version: 0.26.0 - resolution: "scheduler@npm:0.26.0" - checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 +"scheduler@npm:^0.27.0": + version: 0.27.0 + resolution: "scheduler@npm:0.27.0" + checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 languageName: node linkType: hard @@ -13450,10 +14109,10 @@ __metadata: languageName: node linkType: hard -"smol-toml@npm:^1.3.4": - version: 1.4.0 - resolution: "smol-toml@npm:1.4.0" - checksum: 10c0/5f46599c8404ab9e4f9328b3a4f84d0eb01764279826424d225c5c2f3e36d0ee5533eda81aa6507fb24597fd0909611a57273f513a03545e1ad54ee4642ad94f +"smol-toml@npm:^1.5.2": + version: 1.6.0 + resolution: "smol-toml@npm:1.6.0" + checksum: 10c0/baf33bb6cd914d481329e31998a12829cd126541458ba400791212c80f1245d5b27dac04a56a52c02b287d2a494f1628c05fc19643286b258b2e0bb9fe67747c languageName: node linkType: hard @@ -13794,10 +14453,10 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:5.0.2": - version: 5.0.2 - resolution: "strip-json-comments@npm:5.0.2" - checksum: 10c0/e9841b8face78a01b0eb66f81e0a3419186a96f1d26817a5e1f5260b0631c10e0a7f711dddc5988edf599e5c079e4dd6e91defd21523e556636ba5679786f5ac +"strip-json-comments@npm:5.0.3": + version: 5.0.3 + resolution: "strip-json-comments@npm:5.0.3" + checksum: 10c0/daaf20b29f69fb51112698f4a9a662490dbb78d5baf6127c75a0a83c2ac6c078a8c0f74b389ad5e0519d6fc359c4a57cb9971b1ae201aef62ce45a13247791e0 languageName: node linkType: hard @@ -14105,12 +14764,12 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.1.0": - version: 2.1.0 - resolution: "ts-api-utils@npm:2.1.0" +"ts-api-utils@npm:^2.2.0": + version: 2.4.0 + resolution: "ts-api-utils@npm:2.4.0" peerDependencies: typescript: ">=4.8.4" - checksum: 10c0/9806a38adea2db0f6aa217ccc6bc9c391ddba338a9fe3080676d0d50ed806d305bb90e8cef0276e793d28c8a929f400abb184ddd7ff83a416959c0f4d2ce754f + checksum: 10c0/ed185861aef4e7124366a3f6561113557a57504267d4d452a51e0ba516a9b6e713b56b4aeaab9fa13de9db9ab755c65c8c13a777dba9133c214632cb7b65c083 languageName: node linkType: hard @@ -14294,43 +14953,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.4": - version: 5.8.2 - resolution: "typescript@npm:5.8.2" +"typescript@npm:^5.0.4, typescript@npm:^5.8.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6 + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 languageName: node linkType: hard -"typescript@npm:^5.8.3": - version: 5.8.3 - resolution: "typescript@npm:5.8.3" +"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin": - version: 5.8.2 - resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/5448a08e595cc558ab321e49d4cac64fb43d1fa106584f6ff9a8d8e592111b373a995a1d5c7f3046211c8a37201eb6d0f1566f15cdb7a62a5e3be01d087848e2 - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": - version: 5.8.3 - resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 languageName: node linkType: hard @@ -14363,13 +15002,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.8.0": - version: 7.8.0 - resolution: "undici-types@npm:7.8.0" - checksum: 10c0/9d9d246d1dc32f318d46116efe3cfca5a72d4f16828febc1918d94e58f6ffcf39c158aa28bf5b4fc52f410446bc7858f35151367bd7a49f21746cab6497b709b - languageName: node - linkType: hard - "undici@npm:^5.25.4": version: 5.29.0 resolution: "undici@npm:5.29.0" @@ -14506,6 +15138,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.2.0": + version: 1.2.3 + resolution: "update-browserslist-db@npm:1.2.3" + dependencies: + escalade: "npm:^3.2.0" + picocolors: "npm:^1.1.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10c0/13a00355ea822388f68af57410ce3255941d5fb9b7c49342c4709a07c9f230bbef7f7499ae0ca7e0de532e79a82cc0c4edbd125f1a323a1845bf914efddf8bec + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -14746,23 +15392,23 @@ __metadata: linkType: hard "vite-plugin-svgr@npm:^4.0.0": - version: 4.3.0 - resolution: "vite-plugin-svgr@npm:4.3.0" + version: 4.5.0 + resolution: "vite-plugin-svgr@npm:4.5.0" dependencies: - "@rollup/pluginutils": "npm:^5.1.3" + "@rollup/pluginutils": "npm:^5.2.0" "@svgr/core": "npm:^8.1.0" "@svgr/plugin-jsx": "npm:^8.1.0" peerDependencies: vite: ">=2.6.0" - checksum: 10c0/a73f10d319f72cd8c16bf9701cf18170f2300f98c72c6bf939565de0b1e93916bd70c6f5a446dc034b4405c72d382655c7c16be4bd1cbf35bbcde5febf7aeffc + checksum: 10c0/3e1959fec626bb4f5a8ec13ff15bc40ffbc1c0ff38149bebe3f37dc2d67ed1f276f129ff7983e06946cf712e19996affd9d6868aa7d20d8921d1fe4449109b55 languageName: node linkType: hard "vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0, vite@npm:^7.0.0": - version: 7.1.12 - resolution: "vite@npm:7.1.12" + version: 7.3.0 + resolution: "vite@npm:7.3.0" dependencies: - esbuild: "npm:^0.25.0" + esbuild: "npm:^0.27.0" fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" picomatch: "npm:^4.0.3" @@ -14809,7 +15455,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/cef4d4b4a84e663e09b858964af36e916892ac8540068df42a05ced637ceeae5e9ef71c72d54f3cfc1f3c254af16634230e221b6e2327c2a66d794bb49203262 + checksum: 10c0/0457c196cdd5761ec351c0f353945430fbad330e615b9eeab729c8ae163334f18acdc1d9cd7d9d673dbf111f07f6e4f0b25d4ac32360e65b4a6df9991046f3ff languageName: node linkType: hard @@ -15314,18 +15960,16 @@ __metadata: languageName: node linkType: hard -"zod-validation-error@npm:^3.0.3": - version: 3.4.0 - resolution: "zod-validation-error@npm:3.4.0" - peerDependencies: - zod: ^3.18.0 - checksum: 10c0/aaadb0e65c834aacb12fa088663d52d9f4224b5fe6958f09b039f4ab74145fda381c8a7d470bfddf7ddd9bbb5fdfbb52739cd66958ce6d388c256a44094d1fba - languageName: node - linkType: hard - "zod@npm:^3.22.4": version: 3.24.2 resolution: "zod@npm:3.24.2" checksum: 10c0/c638c7220150847f13ad90635b3e7d0321b36cce36f3fc6050ed960689594c949c326dfe2c6fa87c14b126ee5d370ccdebd6efb304f41ef5557a4aaca2824565 languageName: node linkType: hard + +"zod@npm:^4.1.11": + version: 4.3.4 + resolution: "zod@npm:4.3.4" + checksum: 10c0/a096102c8b31ecdb913bacb08d5a0fe8447bbe4f54cff421a0c5830a5552da76aae9fd8a01f2e9fdeaae35da1a73762551bc9d14cfedb13a44056de1ed2eb76f + languageName: node + linkType: hard From 5fa170c9c96c7ed08b31dd27805c3ce39c991f26 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 5 Jan 2026 13:54:08 +0100 Subject: [PATCH 14/76] Fix builds --- src/initializer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/initializer.tsx b/src/initializer.tsx index d0797e9d..267d818a 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -16,7 +16,7 @@ import LanguageDetector from "i18next-browser-languagedetector"; import * as Sentry from "@sentry/react"; import { logger } from "matrix-js-sdk/lib/logger"; import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill"; -import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill"; +import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill.js"; import { useLocation, useNavigationType, @@ -123,7 +123,7 @@ export class Initializer { } if (shouldPolyfillDurationFormat()) { - polyfills.push(import("@formatjs/intl-durationformat/polyfill-force")); + polyfills.push(import("@formatjs/intl-durationformat/polyfill-force.js")); } await Promise.all(polyfills); From b6ca0c4cd6bc460417603f95e2dc9c3e81171d59 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 5 Jan 2026 13:55:58 +0100 Subject: [PATCH 15/76] Fix tests --- src/__snapshots__/AppBar.test.tsx.snap | 2 +- src/__snapshots__/Modal.test.tsx.snap | 12 ++--- src/__snapshots__/Toast.test.tsx.snap | 6 +-- .../ReactionToggleButton.test.tsx.snap | 10 ++-- .../__snapshots__/InCallView.test.tsx.snap | 14 +++--- .../DeveloperSettingsTab.test.tsx.snap | 48 +++++++++---------- src/vitest.setup.ts | 2 +- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/__snapshots__/AppBar.test.tsx.snap b/src/__snapshots__/AppBar.test.tsx.snap index fe61d09b..25bb54ed 100644 --- a/src/__snapshots__/AppBar.test.tsx.snap +++ b/src/__snapshots__/AppBar.test.tsx.snap @@ -13,7 +13,7 @@ exports[`AppBar > renders 1`] = ` class="nav leftNav" > - +
diff --git a/src/UrlParams.ts b/src/UrlParams.ts index f78841fb..048e802b 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -261,7 +261,8 @@ interface IntentAndPlatformDerivedConfiguration { // clearer what each flag means, and helps us avoid coupling Element Call's // behavior to the needs of specific consumers. export interface UrlParams - extends UrlProperties, + extends + UrlProperties, UrlConfiguration, IntentAndPlatformDerivedConfiguration {} diff --git a/src/grid/Grid.tsx b/src/grid/Grid.tsx index 6c85b8af..91fd55ca 100644 --- a/src/grid/Grid.tsx +++ b/src/grid/Grid.tsx @@ -200,8 +200,11 @@ interface Drag { export type DragCallback = (drag: Drag) => void; -interface LayoutMemoProps - extends LayoutProps { +interface LayoutMemoProps< + LayoutModel, + TileModel, + R extends HTMLElement, +> extends LayoutProps { Layout: ComponentType>; } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index c6f22b43..fdbd4461 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -113,8 +113,10 @@ const logger = rootLogger.getChild("[InCallView]"); const maxTapDurationMs = 400; -export interface ActiveCallProps - extends Omit { +export interface ActiveCallProps extends Omit< + InCallViewProps, + "vm" | "livekitRoom" | "connState" +> { e2eeSystem: EncryptionSystem; // TODO refactor those reasons into an enum onLeft: ( diff --git a/src/state/MediaDevices.ts b/src/state/MediaDevices.ts index 2349e361..cea97519 100644 --- a/src/state/MediaDevices.ts +++ b/src/state/MediaDevices.ts @@ -212,9 +212,10 @@ class AudioInput implements MediaDevice { } } -class AudioOutput - implements MediaDevice -{ +class AudioOutput implements MediaDevice< + AudioOutputDeviceLabel, + SelectedAudioOutputDevice +> { private logger = rootLogger.getChild("[MediaDevices AudioOutput]"); public readonly available$ = this.scope.behavior( availableRawDevices$( @@ -274,9 +275,10 @@ class AudioOutput } } -class ControlledAudioOutput - implements MediaDevice -{ +class ControlledAudioOutput implements MediaDevice< + AudioOutputDeviceLabel, + SelectedAudioOutputDevice +> { private logger = rootLogger.getChild("[MediaDevices ControlledAudioOutput]"); // We need to subscribe to the raw devices so that the OS does update the input // back to what it was before. otherwise we will switch back to the default diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 8109784f..2042e819 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -70,8 +70,7 @@ interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps { videoFit: "contain" | "cover"; } -interface SpotlightLocalUserMediaItemProps - extends SpotlightUserMediaItemBaseProps { +interface SpotlightLocalUserMediaItemProps extends SpotlightUserMediaItemBaseProps { vm: LocalUserMediaViewModel; } @@ -85,8 +84,7 @@ const SpotlightLocalUserMediaItem: FC = ({ SpotlightLocalUserMediaItem.displayName = "SpotlightLocalUserMediaItem"; -interface SpotlightRemoteUserMediaItemProps - extends SpotlightUserMediaItemBaseProps { +interface SpotlightRemoteUserMediaItemProps extends SpotlightUserMediaItemBaseProps { vm: RemoteUserMediaViewModel; } From 5d5d75ebdf3dab22f810eff4c64e8219c009bf74 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 5 Jan 2026 21:08:33 +0100 Subject: [PATCH 24/76] fixup merge --- sdk/main.ts | 7 +-- src/livekit/openIDSFU.test.ts | 7 +++ .../localMember/LocalTransport.test.ts | 19 +++++++- .../localMember/LocalTransport.ts | 47 +++++++++++-------- 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/sdk/main.ts b/sdk/main.ts index 376674a4..8d07ab07 100644 --- a/sdk/main.ts +++ b/sdk/main.ts @@ -101,12 +101,7 @@ export async function createMatrixRTCSdk( const mediaDevices = new MediaDevices(scope); const muteStates = new MuteStates(scope, mediaDevices, constant(true)); const slot = { application, id }; - const rtcSession = new MatrixRTCSession( - client, - room, - MatrixRTCSession.sessionMembershipsForSlot(room, slot), - slot, - ); + const rtcSession = new MatrixRTCSession(client, room, slot); const callViewModel = createCallViewModel$( scope, rtcSession, diff --git a/src/livekit/openIDSFU.test.ts b/src/livekit/openIDSFU.test.ts index 2a260b01..8b263662 100644 --- a/src/livekit/openIDSFU.test.ts +++ b/src/livekit/openIDSFU.test.ts @@ -18,6 +18,7 @@ import fetchMock from "fetch-mock"; import { getSFUConfigWithOpenID, type OpenIDClientParts } from "./openIDSFU"; import { testJWTToken } from "../utils/test-fixtures"; +import { ownMemberMock } from "../utils/test"; const sfuUrl = "https://sfu.example.org"; @@ -42,7 +43,9 @@ describe("getSFUConfigWithOpenID", () => { }); const config = await getSFUConfigWithOpenID( matrixClient, + ownMemberMock, "https://sfu.example.org", + false, "!example_room_id", ); expect(config).toEqual({ @@ -63,7 +66,9 @@ describe("getSFUConfigWithOpenID", () => { try { await getSFUConfigWithOpenID( matrixClient, + ownMemberMock, "https://sfu.example.org", + false, "!example_room_id", ); } catch (ex) { @@ -98,7 +103,9 @@ describe("getSFUConfigWithOpenID", () => { }); const config = await getSFUConfigWithOpenID( matrixClient, + ownMemberMock, "https://sfu.example.org", + false, "!example_room_id", ); expect(config).toEqual({ diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index c77297ef..2199ca94 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -49,6 +49,8 @@ describe("LocalTransport", () => { useOldestMember$: constant(false), memberships$: constant(new Epoch([])), client: { + // eslint-disable-next-line @typescript-eslint/naming-convention + _unstable_getRTCTransports: async () => Promise.resolve([]), getDomain: () => "", baseUrl: "example.org", // These won't be called in this error path but satisfy the type @@ -130,6 +132,8 @@ describe("LocalTransport", () => { useOldestMember$: constant(false), memberships$: constant(new Epoch([])), client: { + // eslint-disable-next-line @typescript-eslint/naming-convention + _unstable_getRTCTransports: async () => Promise.resolve([]), getDomain: () => "", getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), @@ -140,7 +144,12 @@ describe("LocalTransport", () => { delayId$: constant("delay_id_mock"), }); - openIdResolver.resolve?.({ url: "https://lk.example.org", jwt: "jwt" }); + openIdResolver.resolve?.({ + url: "https://lk.example.org", + jwt: "jwt", + livekitAlias: "!room:example.org", + livekitIdentity: ownMemberMock.userId + ":" + ownMemberMock.deviceId, + }); expect(localTransport$.value).toBe(null); await flushPromises(); // final @@ -203,11 +212,15 @@ describe("LocalTransport", () => { mockConfig({}); customLivekitUrl.setValue(customLivekitUrl.defaultValue); localTransportOpts = { + ownMembershipIdentity: ownMemberMock, scope, roomId: "!example_room_id", useOldestMember$: constant(false), + useOldJwtEndpoint$: constant(false), + delayId$: constant(null), memberships$: constant(new Epoch([])), client: { + baseUrl: "https://example.org", getDomain: vi.fn().mockReturnValue(""), // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), @@ -317,11 +330,15 @@ describe("LocalTransport", () => { it("throws if no options are available", async () => { const localTransport$ = createLocalTransport$({ scope, + ownMembershipIdentity: ownMemberMock, roomId: "!example_room_id", useOldestMember$: constant(false), + useOldJwtEndpoint$: constant(false), + delayId$: constant(null), memberships$: constant(new Epoch([])), client: { getDomain: () => "", + baseUrl: "https://example.org", // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: async () => Promise.resolve([]), // These won't be called in this error path but satisfy the type diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index d8b5dfce..1853ff6d 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -178,7 +178,6 @@ async function makeTransport( forceOldJwtEndpoint: boolean, delayId?: string, ): Promise { - let transport: LivekitTransport | undefined; logger.trace("Searching for a preferred transport"); // We will call `getSFUConfigWithOpenID` once per transport here as it's our @@ -194,32 +193,47 @@ async function makeTransport( logger.info("Using LiveKit transport from dev tools: ", urlFromDevSettings); // Validate that the SFU is up. Otherwise, we want to fail on this // as we don't permit other SFUs. - const config = await getSFUConfigWithOpenID( + // This will call the jwt/sfu/get endpoint to pre create the livekit room. + const { livekitAlias } = await getSFUConfigWithOpenID( client, + membership, urlFromDevSettings, + forceOldJwtEndpoint, roomId, + client.baseUrl, + delayId, + logger, ); return { type: "livekit", livekit_service_url: urlFromDevSettings, - livekit_alias: config.livekitAlias, + livekit_alias: livekitAlias, + forceOldJwtEndpoint, }; } async function getFirstUsableTransport( transports: Transport[], - ): Promise { + ): Promise<(LivekitTransport & { forceOldJwtEndpoint: boolean }) | null> { for (const potentialTransport of transports) { if (isLivekitTransportConfig(potentialTransport)) { try { + // This will call the jwt/sfu/get endpoint to pre create the livekit room. const { livekitAlias } = await getSFUConfigWithOpenID( client, + membership, potentialTransport.livekit_service_url, + forceOldJwtEndpoint, roomId, + client.baseUrl, + delayId, + logger, ); + return { ...potentialTransport, livekit_alias: livekitAlias, + forceOldJwtEndpoint, }; } catch (ex) { if (ex instanceof FailToGetOpenIdToken) { @@ -283,10 +297,16 @@ async function makeTransport( const urlFromConf = Config.get().livekit?.livekit_service_url; if (urlFromConf) { try { + // This will call the jwt/sfu/get endpoint to pre create the livekit room. const { livekitAlias } = await getSFUConfigWithOpenID( client, + membership, urlFromConf, + forceOldJwtEndpoint, roomId, + client.baseUrl, + delayId, + logger, ); const selectedTransport: LivekitTransport = { type: "livekit", @@ -294,7 +314,7 @@ async function makeTransport( livekit_alias: livekitAlias, }; logger.info("Using config SFU", selectedTransport); - return selectedTransport; + return { ...selectedTransport, forceOldJwtEndpoint }; } catch (ex) { if (ex instanceof FailToGetOpenIdToken) { throw ex; @@ -303,19 +323,6 @@ async function makeTransport( } } - if (!transport) throw new MatrixRTCTransportMissingError(domain ?? ""); - - // this will call the jwt/sfu/get endpoint to pre create the livekit room. - await getSFUConfigWithOpenID( - client, - membership, - transport.livekit_service_url, - forceOldJwtEndpoint, - transport.livekit_alias, - client.baseUrl, - delayId, - logger, - ); - - return { ...transport, forceOldJwtEndpoint }; + // If we do not have returned a transport by now we throw an error + throw new MatrixRTCTransportMissingError(domain ?? ""); } From 00fca6e3c731e24ed1ceffddde33e690412d3589 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 5 Jan 2026 21:17:37 +0100 Subject: [PATCH 25/76] simplify localTransport --- .../localMember/LocalTransport.ts | 73 +++++++------------ 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 1853ff6d..50d6cec6 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -180,6 +180,26 @@ async function makeTransport( ): Promise { logger.trace("Searching for a preferred transport"); + async function doOpenIdAndJWTFromUrl( + url: string, + ): Promise { + const { livekitAlias } = await getSFUConfigWithOpenID( + client, + membership, + url, + forceOldJwtEndpoint, + roomId, + client.baseUrl, + delayId, + logger, + ); + return { + type: "livekit", + livekit_service_url: url, + livekit_alias: livekitAlias, + forceOldJwtEndpoint, + }; + } // We will call `getSFUConfigWithOpenID` once per transport here as it's our // only mechanism of valiation. This means we will also ask the // homeserver for a OpenID token a few times. Since OpenID tokens are single @@ -190,26 +210,11 @@ async function makeTransport( // DEVTOOL: Highest priority: Load from devtool setting if (urlFromDevSettings !== null) { - logger.info("Using LiveKit transport from dev tools: ", urlFromDevSettings); // Validate that the SFU is up. Otherwise, we want to fail on this // as we don't permit other SFUs. // This will call the jwt/sfu/get endpoint to pre create the livekit room. - const { livekitAlias } = await getSFUConfigWithOpenID( - client, - membership, - urlFromDevSettings, - forceOldJwtEndpoint, - roomId, - client.baseUrl, - delayId, - logger, - ); - return { - type: "livekit", - livekit_service_url: urlFromDevSettings, - livekit_alias: livekitAlias, - forceOldJwtEndpoint, - }; + logger.info("Using LiveKit transport from dev tools: ", urlFromDevSettings); + return await doOpenIdAndJWTFromUrl(urlFromDevSettings); } async function getFirstUsableTransport( @@ -219,22 +224,9 @@ async function makeTransport( if (isLivekitTransportConfig(potentialTransport)) { try { // This will call the jwt/sfu/get endpoint to pre create the livekit room. - const { livekitAlias } = await getSFUConfigWithOpenID( - client, - membership, + return await doOpenIdAndJWTFromUrl( potentialTransport.livekit_service_url, - forceOldJwtEndpoint, - roomId, - client.baseUrl, - delayId, - logger, ); - - return { - ...potentialTransport, - livekit_alias: livekitAlias, - forceOldJwtEndpoint, - }; } catch (ex) { if (ex instanceof FailToGetOpenIdToken) { // Explictly throw these @@ -298,23 +290,8 @@ async function makeTransport( if (urlFromConf) { try { // This will call the jwt/sfu/get endpoint to pre create the livekit room. - const { livekitAlias } = await getSFUConfigWithOpenID( - client, - membership, - urlFromConf, - forceOldJwtEndpoint, - roomId, - client.baseUrl, - delayId, - logger, - ); - const selectedTransport: LivekitTransport = { - type: "livekit", - livekit_service_url: urlFromConf, - livekit_alias: livekitAlias, - }; - logger.info("Using config SFU", selectedTransport); - return { ...selectedTransport, forceOldJwtEndpoint }; + logger.info("Using config SFU", urlFromConf); + return await doOpenIdAndJWTFromUrl(urlFromConf); } catch (ex) { if (ex instanceof FailToGetOpenIdToken) { throw ex; From 15800872862fccd5839b4f64ad3cb8b27a570140 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 5 Jan 2026 21:24:52 +0100 Subject: [PATCH 26/76] use latest js-sdk --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 95c30ef6..1d4d6393 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=6f0815930a008eff8f86e6e5748d447be0e7c25e", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.14.0", "node-stdlib-browser": "^1.3.1", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 1a200472..6c0e4948 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7951,7 +7951,7 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=6f0815930a008eff8f86e6e5748d447be0e7c25e" + matrix-js-sdk: "matrix-org/matrix-js-sdk#develop" matrix-widget-api: "npm:^1.14.0" node-stdlib-browser: "npm:^1.3.1" normalize.css: "npm:^8.0.1" @@ -10939,9 +10939,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/use-membershipID-for-session-state-events&commit=6f0815930a008eff8f86e6e5748d447be0e7c25e": +"matrix-js-sdk@matrix-org/matrix-js-sdk#develop": version: 39.4.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=6f0815930a008eff8f86e6e5748d447be0e7c25e" + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=4b89fb23c54aaf7826bd127d8fa21cc7bb87688f" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" @@ -10957,7 +10957,7 @@ __metadata: sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/a5a904a79f3660d1f6fe217195e662adf82af4a445681e47f292772d9d4d63ce60aaca209f40c41e2d659bee2b17cd5b3345bbad77795032057f2c0e3129cc77 + checksum: 10c0/bc7443bf67822e9bc7b8e531b4e61e6ebac41c2fd8047ac0567456c264ae0d1911fbef6e437d312a3adeead86cd5e7134944e3fd73d28002777618bc0ebaa1ca languageName: node linkType: hard From 69a4189517ca83016053b8d2f1722e2324076d45 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 5 Jan 2026 21:58:26 +0100 Subject: [PATCH 27/76] self review --- src/e2ee/matrixKeyProvider.ts | 4 ++-- src/livekit/openIDSFU.ts | 29 ++++++++++++++---------- src/room/InCallView.test.tsx | 2 +- src/room/InCallView.tsx | 1 - src/state/CallViewModel/CallViewModel.ts | 3 ++- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index d7aebc4b..63a96755 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -61,12 +61,12 @@ export class MatrixKeyProvider extends BaseKeyProvider { ); logger.debug( - `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${rtcBackendIdentity} (before hash: ${membershipParts.userId}) encryptionKeyIndex=${encryptionKeyIndex}`, + `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${rtcBackendIdentity} (before hash: ${membershipParts.userId}:${membershipParts.deviceId}) encryptionKeyIndex=${encryptionKeyIndex}`, ); }, (e) => { logger.error( - `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipParts.userId} encryptionKeyIndex=${encryptionKeyIndex}`, + `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipParts.userId}:${membershipParts.deviceId} encryptionKeyIndex=${encryptionKeyIndex}`, e, ); }, diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 7d6dfc24..6728a243 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -20,6 +20,7 @@ export interface SFUConfig { url: string; jwt: string; livekitAlias: string; + // NOTE: Currently unused. livekitIdentity: string; } @@ -68,7 +69,7 @@ export type OpenIDClientParts = Pick< * @param client The Matrix client * @param membership * @param serviceUrl The URL of the livekit SFU service - * @param forceOldEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination + * @param forceOldJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination * instead of a hash. * This function by default uses whatever is possible with the current jwt service installed next to the SFU. * For remote connections this does not matter, since we will not publish there we can rely on the newest option. @@ -103,12 +104,6 @@ export async function getSFUConfigWithOpenID( logger?.debug("Got openID token", openIdToken); logger?.info(`Trying to get JWT for focus ${serviceUrl}...`); - const args: [CallMembershipIdentityParts, string, string, IOpenIDToken] = [ - membership, - serviceUrl, - roomId, - openIdToken, - ]; let sfuConfig: { url: string; jwt: string }; try { @@ -118,7 +113,10 @@ export async function getSFUConfigWithOpenID( throw new Error("No delayId, Won't try matrix 2.0 jwt endpoint."); sfuConfig = await getLiveKitJWTWithDelayDelegation( - ...args, + membership, + serviceUrl, + roomId, + openIdToken, delayEndpointBaseUrl, delayId, ); @@ -128,23 +126,30 @@ export async function getSFUConfigWithOpenID( `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy)`, e, ); - sfuConfig = await getLiveKitJWT(...args); + sfuConfig = await getLiveKitJWT( + membership.deviceId, + serviceUrl, + roomId, + openIdToken, + ); logger?.info(`Got JWT from call's active focus URL.`); } // Pull the details from the JWT const [, payloadStr] = sfuConfig.jwt.split("."); - + // TODO: Prefer Uint8Array.fromBase64 when widely available const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload; return { jwt: sfuConfig.jwt, url: sfuConfig.url, livekitAlias: payload.video.room, // NOTE: Currently unused. + // Probably also not helpful since we now compute the backendIdentity on joining the call so we can use it for the encryption manager. + // The only reason for us to know it locally is to connect the right users with the lk world. (and to set our own keys) livekitIdentity: payload.sub, }; } async function getLiveKitJWT( - membership: CallMembershipIdentityParts, + deviceId: string, livekitServiceURL: string, matrixRoomId: string, openIDToken: IOpenIDToken, @@ -159,7 +164,7 @@ async function getLiveKitJWT( // This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used. room: matrixRoomId, openid_token: openIDToken, - device_id: membership.deviceId, + device_id: deviceId, }), }); if (!res.ok) { diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index 8ac4bccb..a137074b 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -22,6 +22,7 @@ import { BrowserRouter } from "react-router-dom"; import { TooltipProvider } from "@vector-im/compound-web"; import { RoomContext, useLocalParticipant } from "@livekit/components-react"; +import { InCallView } from "./InCallView"; import { mockLivekitRoom, mockLocalParticipant, @@ -33,7 +34,6 @@ import { mockRtcMembership, type MockRTCSession, } from "../utils/test"; -import { InCallView } from "./InCallView"; import { E2eeType } from "../e2ee/e2eeType"; import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; import { alice, local } from "../utils/test-fixtures"; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index a21c3302..fdbd4461 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -795,7 +795,6 @@ export const InCallView: FC = ({ onTouchEnd={onControlsTouchEnd} /> )} - {!showControls &&
}
); diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 9c2fbc3d..093abfad 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -423,7 +423,8 @@ export function createCallViewModel$( const ownMembershipIdentity: CallMembershipIdentityParts = { userId, deviceId, - // TODO look into this!!! + // This will eventually become the salt for the hash endpoint. + // For now we keep it as the user+device string since it is expected by non matrix matrixRTCMode === Legacy. memberId: `${userId}:${deviceId}`, }; From 556a87d1411d30e9b5aa600e33c97e2dfebce930 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 5 Jan 2026 22:20:19 +0100 Subject: [PATCH 28/76] fix js-doc --- src/livekit/openIDSFU.ts | 8 ++++---- src/state/CallViewModel/localMember/LocalMember.ts | 8 ++++---- src/state/CallViewModel/localMember/LocalTransport.ts | 6 ++++-- .../CallViewModel/remoteMembers/ConnectionFactory.ts | 11 ++++++----- .../CallViewModel/remoteMembers/ConnectionManager.ts | 10 +++++++--- .../remoteMembers/ECConnectionFactory.test.ts | 4 ++-- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 6728a243..b89243c1 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -67,7 +67,7 @@ export type OpenIDClientParts = Pick< * to the matrix RTC backend in order to get acces to the SFU. * It has built-in retry for calls to the homeserver with a backoff policy. * @param client The Matrix client - * @param membership + * @param membership Our own membership identity parts used to send to jwt service. * @param serviceUrl The URL of the livekit SFU service * @param forceOldJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination * instead of a hash. @@ -75,9 +75,9 @@ export type OpenIDClientParts = Pick< * For remote connections this does not matter, since we will not publish there we can rely on the newest option. * For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events. * @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases. - * @param delayEndpointBaseUrl - * @param delayId - * @param logger + * @param delayEndpointBaseUrl The URL of the matrix homeserver. + * @param delayId The delay id used for the jwt service to manage. + * @param logger optional logger. * @returns Object containing the token information * @throws FailToGetOpenIdToken */ diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 890165dd..17f766ff 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -687,10 +687,9 @@ interface EnterRTCSessionOptions { * - Handles retries (fails only after several attempts) * * @param rtcSession - The MatrixRTCSession to join. + * @param ownMembershipIdentity - Options for entering the RTC session. * @param transport - The LivekitTransport to use for this session. - * @param options - Options for entering the RTC session. - * @param options.encryptMedia - Whether to encrypt media. - * @param options.matrixRTCMode - The Matrix RTC mode to use. + * @param options - `encryptMedia`: Whether to encrypt media `matrixRTCMode`: The Matrix RTC mode to use. * @throws If the widget could not send ElementWidgetActions.JoinCall action. */ // Exported for unit testing @@ -698,8 +697,9 @@ export function enterRTCSession( rtcSession: MatrixRTCSession, ownMembershipIdentity: CallMembershipIdentityParts, transport: LivekitTransport, - { encryptMedia, matrixRTCMode }: EnterRTCSessionOptions, + options: EnterRTCSessionOptions, ): void { + const { encryptMedia, matrixRTCMode } = options; PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId); diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 50d6cec6..fa316805 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -159,10 +159,12 @@ const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; * 4. The transport configured in Element Call's config. * * @param client The authenticated Matrix client for the current user + * @param membership The membership identity of the user. * @param roomId The ID of the room to be connected to. * @param urlFromDevSettings Override URL provided by the user's local config. - * @param useMatrix2 This implies using the matrix2 jwt endpoint (including delayed event delegation of the jwt token) - * @param delayId + * @param forceOldJwtEndpoint Whether to force the old JWT endpoint (not hashing the backendIdentity). + * @param delayId the delay id passed to the jwt service. + * * @returns A fully validated transport config. * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ diff --git a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts index 8e9c0dab..668538ac 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts @@ -31,8 +31,8 @@ export interface ConnectionFactory { createConnection( transport: LivekitTransport, scope: ObservableScope, - logger: Logger, ownMembershipIdentity: CallMembershipIdentityParts, + logger: Logger, forceOldJwtEndpoint?: boolean, ): Connection; } @@ -83,17 +83,18 @@ export class ECConnectionFactory implements ConnectionFactory { /** * - * @param transport - * @param scope - * @param logger + * @param transport The transport to use for this connection. + * @param scope The observable scope (used for clean-up) * @param ownMembershipIdentity required to connect (using the jwt service) with the SFU. + * @param logger The logger instance to use for this connection. + * @param forceOldJwtEndpoint Use the old JWT endpoint independent of what the sfu supports. * @returns */ public createConnection( transport: LivekitTransport, scope: ObservableScope, - logger: Logger, ownMembershipIdentity: CallMembershipIdentityParts, + logger: Logger, forceOldJwtEndpoint?: boolean, ): Connection { return new Connection( diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index aa5a15ba..9d546d24 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -83,8 +83,12 @@ export interface IConnectionManager { * @param props - Configuration object * @param props.scope - The observable scope used by this object * @param props.connectionFactory - Used to create new connections - * @param props.inputTransports$ - A list of Behaviors each containing a LIST of LivekitTransport. - * @param props.logger - The logger to use + * @param props.localTransport$ - The local transport to use. (deduplicated with remoteTransports$) + * @param props.remoteTransports$ - All other transports. The connection manager will create connections for each transport. (deduplicated with localTransport$) + * @param props.ownMembershipIdentity - The own membership identity to use. + * @param props.logger - The logger to use. + * @param props.forceOldJwtEndpointForLocalTransport$ - Use the old JWT endpoint independent of what the sfu supports. Only applies for localTransport$. + * * Each of these behaviors can be interpreted as subscribed list of transports. * * Using `registerTransports` independent external modules can control what connections @@ -183,8 +187,8 @@ export function createConnectionManager$({ livekit_alias: alias, }, scope, - logger, ownMembershipIdentity, + logger, forceOldJwtEndpoint, ); // Start the connection immediately diff --git a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts index 3c60e776..f28bd158 100644 --- a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts +++ b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts @@ -79,8 +79,8 @@ describe("ECConnectionFactory - Audio inputs options", () => { ecConnectionFactory.createConnection( exampleTransport, testScope, - logger, ownMemberMock, + logger, ); // Check if Room was constructed with expected options @@ -125,8 +125,8 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => { ecConnectionFactory.createConnection( exampleTransport, testScope, - logger, ownMemberMock, + logger, ); // Check if Room was constructed with expected options From 83d04ac1222784de3af2997e2021ebf2a8766fdd Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 13:26:37 +0100 Subject: [PATCH 29/76] fix tests and remove duplicated mocks. --- .../localMember/LocalMember.test.ts | 11 ++- .../localMember/LocalTransport.test.ts | 6 ++ .../localMember/LocalTransport.ts | 6 +- .../remoteMembers/ConnectionManager.ts | 8 +- .../MatrixLivekitMembers.test.ts | 6 +- .../MatrixMemberMetadata.test.ts | 72 ++++++++-------- .../remoteMembers/integration.test.ts | 8 +- src/utils/test-fixtures.ts | 14 ++-- src/utils/test.ts | 84 +++++++------------ 9 files changed, 103 insertions(+), 112 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index edd5ea1e..8a7505eb 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -104,7 +104,7 @@ describe("LocalMembership", () => { getOldestMembership: vi.fn().mockReturnValue({ getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership]), }), - joinRoomSession: vi.fn(), + joinRTCSession: vi.fn(), }) as unknown as MatrixRTCSession; enterRTCSession( @@ -121,7 +121,12 @@ describe("LocalMembership", () => { }, ); - expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith( + expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith( + { + deviceId: "DEVICE", + memberId: "@alice:example.org:DEVICE", + userId: "@alice:example.org", + }, [ { livekit_alias: "roomId", @@ -163,7 +168,7 @@ describe("LocalMembership", () => { }, memberships: [], getFocusInUse: vi.fn(), - joinRoomSession: vi.fn(), + joinRTCSession: vi.fn(), }) as unknown as MatrixRTCSession; enterRTCSession( diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 2199ca94..c37cab56 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -154,6 +154,7 @@ describe("LocalTransport", () => { await flushPromises(); // final expect(localTransport$.value).toStrictEqual({ + forceOldJwtEndpoint: false, livekit_alias: "!room:example.org", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -195,6 +196,7 @@ describe("LocalTransport", () => { await flushPromises(); // final expect(localTransport$.value).toStrictEqual({ + forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -247,6 +249,7 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ + forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -259,6 +262,7 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ + forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -273,6 +277,7 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ + forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -304,6 +309,7 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ + forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index fa316805..0dae3c99 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -84,7 +84,9 @@ export const createLocalTransport$ = ({ useOldestMember$, useOldJwtEndpoint$, delayId$, -}: Props): Behavior => { +}: Props): Behavior< + (LivekitTransport & { forceOldJwtEndpoint: boolean }) | null +> => { /** * The transport over which we should be actively publishing our media. * undefined when not joined. @@ -108,7 +110,7 @@ export const createLocalTransport$ = ({ * * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ - const preferredTransport$: Behavior = scope.behavior( + const preferredTransport$ = scope.behavior( combineLatest([customLivekitUrl.value$, delayId$, useOldJwtEndpoint$]).pipe( switchMap(([customUrl, delayId, forceOldJwtEndpoint]) => from( diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index 9d546d24..e5a542df 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -144,12 +144,14 @@ export function createConnectionManager$({ localTransport, forceOldJwtEndpointForLocalTransport, ]) => { - // nmodify only the local transport with forceOldJwtEndpointForLocalTransport + // modify only the local transport with forceOldJwtEndpointForLocalTransport const index = transports.value.findIndex((t) => areLivekitTransportsEqual(localTransport, t), ); - transports.value[index].forceOldJwtEndpoint = - forceOldJwtEndpointForLocalTransport; + if (index !== -1) { + transports.value[index].forceOldJwtEndpoint = + forceOldJwtEndpointForLocalTransport; + } logger.trace( `Managing transports: ${transports.value.map((t) => t.livekit_service_url).join(", ")}`, ); diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts index de0d7ecc..55549a10 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts @@ -26,7 +26,7 @@ import { import { ConnectionManagerData } from "./ConnectionManager.ts"; import { flushPromises, - mockCallMembership, + mockRtcMembership, mockRemoteParticipant, } from "../../../utils/test.ts"; import { type Connection } from "./Connection.ts"; @@ -49,12 +49,12 @@ const transportB: LivekitTransport = { livekit_alias: "!alias:sample.com", }; -const bobMembership = mockCallMembership( +const bobMembership = mockRtcMembership( "@bob:example.org", "DEV000", transportA, ); -const carlMembership = mockCallMembership( +const carlMembership = mockRtcMembership( "@carl:sample.com", "DEV111", transportB, diff --git a/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.test.ts b/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.test.ts index 6f392351..f7dd775c 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.test.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.test.ts @@ -18,7 +18,7 @@ import { it } from "vitest"; import { ObservableScope } from "../../ObservableScope.ts"; import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room"; import { - mockCallMembership, + mockRtcMembership, mockMatrixRoomMember, withTestScheduler, } from "../../../utils/test.ts"; @@ -111,7 +111,7 @@ describe("MatrixMemberMetadata", () => { rawDisplayName: "it's a me", }); const memberships$ = behavior("a", { - a: [mockCallMembership("@local:example.com", "DEVICE1")], + a: [mockRtcMembership("@local:example.com", "DEVICE1")], }); const metadataStore = createMatrixMemberMetadata$( testScope, @@ -149,8 +149,8 @@ describe("MatrixMemberMetadata", () => { withTestScheduler(({ behavior, expectObservable }) => { const memberships$ = behavior("a", { a: [ - mockCallMembership("@alice:example.com", "DEVICE1"), - mockCallMembership("@bob:example.com", "DEVICE1"), + mockRtcMembership("@alice:example.com", "DEVICE1"), + mockRtcMembership("@bob:example.com", "DEVICE1"), ], }); const metadataStore = createMatrixMemberMetadata$( @@ -179,7 +179,7 @@ describe("MatrixMemberMetadata", () => { setUpBasicRoom(); const memberships$ = behavior("a", { - a: [mockCallMembership("@no-name:foo.bar", "D000")], + a: [mockRtcMembership("@no-name:foo.bar", "D000")], }); const metadataStore = createMatrixMemberMetadata$( testScope, @@ -201,11 +201,11 @@ describe("MatrixMemberMetadata", () => { const memberships$ = behavior("a", { a: [ - mockCallMembership("@bob:example.com", "DEVICE1"), - mockCallMembership("@bob:example.com", "DEVICE2"), - mockCallMembership("@bob:foo.bar", "BOB000"), - mockCallMembership("@carl:example.com", "C000"), - mockCallMembership("@evil:example.com", "E000"), + mockRtcMembership("@bob:example.com", "DEVICE1"), + mockRtcMembership("@bob:example.com", "DEVICE2"), + mockRtcMembership("@bob:foo.bar", "BOB000"), + mockRtcMembership("@carl:example.com", "C000"), + mockRtcMembership("@evil:example.com", "E000"), ], }); @@ -233,10 +233,10 @@ describe("MatrixMemberMetadata", () => { setUpBasicRoom(); const memberships$ = behavior("ab", { - a: [mockCallMembership("@bob:example.com", "DEVICE1")], + a: [mockRtcMembership("@bob:example.com", "DEVICE1")], b: [ - mockCallMembership("@bob:example.com", "DEVICE1"), - mockCallMembership("@bob:foo.bar", "BOB000"), + mockRtcMembership("@bob:example.com", "DEVICE1"), + mockRtcMembership("@bob:foo.bar", "BOB000"), ], }); @@ -262,10 +262,10 @@ describe("MatrixMemberMetadata", () => { const memberships$ = behavior("ab", { a: [ - mockCallMembership("@bob:example.com", "DEVICE1"), - mockCallMembership("@bob:foo.bar", "BOB000"), + mockRtcMembership("@bob:example.com", "DEVICE1"), + mockRtcMembership("@bob:foo.bar", "BOB000"), ], - b: [mockCallMembership("@bob:example.com", "DEVICE1")], + b: [mockRtcMembership("@bob:example.com", "DEVICE1")], }); const metadataStore = createMatrixMemberMetadata$( @@ -292,8 +292,8 @@ describe("MatrixMemberMetadata", () => { const memberships$ = behavior("a", { a: [ - mockCallMembership("@bob:example.com", "B000"), - mockCallMembership("@carl:example.com", "C000"), + mockRtcMembership("@bob:example.com", "B000"), + mockRtcMembership("@carl:example.com", "C000"), ], }); const metadataStore = createMatrixMemberMetadata$( @@ -331,16 +331,16 @@ describe("MatrixMemberMetadata", () => { // - room join/leave // - disambiguate const memberships$ = behavior("ab-d", { - a: [mockCallMembership(CARL, "C000")], + a: [mockRtcMembership(CARL, "C000")], b: [ - mockCallMembership(CARL, "C000"), + mockRtcMembership(CARL, "C000"), // bob joins - mockCallMembership(BOB, "B000"), + mockRtcMembership(BOB, "B000"), ], // c carl gets renamed to BOB d: [ // carl leaves - mockCallMembership(BOB, "B000"), + mockRtcMembership(BOB, "B000"), ], }); schedule("--a-", { @@ -379,8 +379,8 @@ describe("MatrixMemberMetadata", () => { it("should disambiguate users with invisible characters", () => { withTestScheduler(({ behavior, expectObservable }) => { - const bobRtcMember = mockCallMembership("@bob:example.org", "BBBB"); - const bobZeroWidthSpaceRtcMember = mockCallMembership( + const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB"); + const bobZeroWidthSpaceRtcMember = mockRtcMembership( "@bob2:example.org", "BBBB", ); @@ -397,9 +397,9 @@ describe("MatrixMemberMetadata", () => { fakeMemberWith(bobZeroWidthSpace); fakeMemberWith({ userId: "@carol:example.org" }); const memberships$ = behavior("ab", { - a: [mockCallMembership("@carol:example.org", "1111"), bobRtcMember], + a: [mockRtcMembership("@carol:example.org", "1111"), bobRtcMember], b: [ - mockCallMembership("@carol:example.org", "1111"), + mockRtcMembership("@carol:example.org", "1111"), bobRtcMember, bobZeroWidthSpaceRtcMember, ], @@ -450,8 +450,8 @@ describe("MatrixMemberMetadata", () => { it("should strip RTL characters from displayname", () => { withTestScheduler(({ behavior, expectObservable }) => { - const daveRtcMember = mockCallMembership("@dave:example.org", "DDDD"); - const daveRTLRtcMember = mockCallMembership( + const daveRtcMember = mockRtcMembership("@dave:example.org", "DDDD"); + const daveRTLRtcMember = mockRtcMembership( "@dave2:example.org", "DDDD", ); @@ -466,9 +466,9 @@ describe("MatrixMemberMetadata", () => { fakeMemberWith(daveRTL); fakeMemberWith(dave); const memberships$ = behavior("ab", { - a: [mockCallMembership("@carol:example.org", "DDDD")], + a: [mockRtcMembership("@carol:example.org", "DDDD")], b: [ - mockCallMembership("@carol:example.org", "DDDD"), + mockRtcMembership("@carol:example.org", "DDDD"), daveRtcMember, daveRTLRtcMember, ], @@ -527,8 +527,8 @@ describe("MatrixMemberMetadata", () => { }); const memberships$ = behavior("a", { a: [ - mockCallMembership("@local:example.com", "DEVICE1"), - mockCallMembership("@alice:example.com", "DEVICE1"), + mockRtcMembership("@local:example.com", "DEVICE1"), + mockRtcMembership("@alice:example.com", "DEVICE1"), ], }); const metadataStore = createMatrixMemberMetadata$( @@ -562,12 +562,12 @@ describe("MatrixMemberMetadata", () => { fakeMemberWith({ userId: "@carl:example.com" }); fakeMemberWith({ userId: "@bob:example.com" }); const memberships$ = behavior("ab-d", { - a: [mockCallMembership("@bob:example.com", "B000")], + a: [mockRtcMembership("@bob:example.com", "B000")], b: [ - mockCallMembership("@bob:example.com", "B000"), - mockCallMembership("@carl:example.com", "C000"), + mockRtcMembership("@bob:example.com", "B000"), + mockRtcMembership("@carl:example.com", "C000"), ], - d: [mockCallMembership("@carl:example.com", "C000")], + d: [mockRtcMembership("@carl:example.com", "C000")], }); const metadataStore = createMatrixMemberMetadata$( diff --git a/src/state/CallViewModel/remoteMembers/integration.test.ts b/src/state/CallViewModel/remoteMembers/integration.test.ts index 84e09487..c29f07c0 100644 --- a/src/state/CallViewModel/remoteMembers/integration.test.ts +++ b/src/state/CallViewModel/remoteMembers/integration.test.ts @@ -21,8 +21,8 @@ import { import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import { - mockCallMembership, mockMediaDevices, + mockRtcMembership, ownMemberMock, withTestScheduler, } from "../../../utils/test.ts"; @@ -101,9 +101,9 @@ afterEach(() => { test("bob, carl, then bob joining no tracks yet", () => { withTestScheduler(({ expectObservable, behavior, scope }) => { - const bobMembership = mockCallMembership("@bob:example.com", "BDEV000"); - const carlMembership = mockCallMembership("@carl:example.com", "CDEV000"); - const daveMembership = mockCallMembership("@dave:foo.bar", "DDEV000"); + const bobMembership = mockRtcMembership("@bob:example.com", "BDEV000"); + const carlMembership = mockRtcMembership("@carl:example.com", "CDEV000"); + const daveMembership = mockRtcMembership("@dave:foo.bar", "DDEV000"); const eMarble = "abc"; const vMarble = "abc"; diff --git a/src/utils/test-fixtures.ts b/src/utils/test-fixtures.ts index f915bb19..dcdb9f9c 100644 --- a/src/utils/test-fixtures.ts +++ b/src/utils/test-fixtures.ts @@ -17,14 +17,16 @@ export const localRtcMemberDevice2 = mockRtcMembership( "2222", ); export const local = mockMatrixRoomMember(localRtcMember); -// export const localParticipant = mockLocalParticipant({ identity: "" }); + export const localId = `${local.userId}:${localRtcMember.deviceId}`; -export const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA"); +export const aliceDeviceId = "AAAA"; +export const aliceUserId = "@alice:example.org"; +export const aliceId = `${aliceUserId}:${aliceDeviceId}`; +export const aliceRtcMember = mockRtcMembership(aliceUserId, aliceDeviceId); export const alice = mockMatrixRoomMember(aliceRtcMember, { rawDisplayName: "Alice", }); -export const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`; export const aliceParticipant = mockRemoteParticipant({ identity: aliceId }); export const aliceDoppelgangerRtcMember = mockRtcMembership( @@ -38,11 +40,13 @@ export const aliceDoppelganger = mockMatrixRoomMember( }, ); -export const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB"); +export const bobDeviceId = "BBBB"; +export const bobUserId = "@bob:example.org"; +export const bobId = `${bobUserId}:${bobDeviceId}`; +export const bobRtcMember = mockRtcMembership(bobUserId, bobDeviceId); export const bob = mockMatrixRoomMember(bobRtcMember, { rawDisplayName: "Bob", }); -export const bobId = `${bob.userId}:${bobRtcMember.deviceId}`; export const bobZeroWidthSpaceRtcMember = mockRtcMembership( "@bob2:example.org", diff --git a/src/utils/test.ts b/src/utils/test.ts index 02277af0..b19ea961 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -203,40 +203,30 @@ export const exampleTransport: LivekitTransport = { livekit_alias: "!alias:example.org", }; -export function mockCallMembership( - userId: string, - deviceId: string, - transport?: Transport, -): CallMembership { - const t = transport ?? transportForUser(userId); - return { - userId: userId, - deviceId: deviceId, - getTransport: vi.fn().mockReturnValue(t), - transports: [t], - } as unknown as CallMembership; -} - -function transportForUser(userId: string): Transport { - const domain = userId.split(":")[1]; - return { - type: "livekit", - livekit_service_url: `https://lk.${domain}`, - livekit_alias: `!alias:${domain}`, - }; -} - export function mockRtcMembership( user: string | RoomMember, deviceId: string, - callId = "", - fociPreferred: Transport[] = [exampleTransport], - focusActive: LivekitFocusSelection = { - type: "livekit", - focus_selection: "oldest_membership", + customOverwrites?: { + rtcBackendIdentity?: string; + callId?: string; + fociPreferred?: Transport[]; + focusActive?: LivekitFocusSelection; + membership?: Partial; }, - membership: Partial = {}, ): CallMembership { + // setup defaults based on overwrites and fallback values. + const { rtcBackendIdentity, callId, fociPreferred, focusActive, membership } = + { + fociPreferred: [exampleTransport], + focusActive: { + type: "livekit" as const, + focus_selection: "oldest_membership" as const, + }, + callId: "", + membership: {}, + ...customOverwrites, + }; + const data: SessionMembershipData = { application: "m.call", call_id: callId, @@ -245,15 +235,21 @@ export function mockRtcMembership( focus_active: focusActive, ...membership, }; + const userId = typeof user === "string" ? user : user.userId; const event = new MatrixEvent({ - sender: typeof user === "string" ? user : user.userId, + sender: userId, event_id: `$-ev-${randomUUID()}:example.org`, content: data, }); const membershipData = CallMembership.membershipDataFromMatrixEvent(event); - const cms = new CallMembership(event, membershipData, "xx"); + const cms = new CallMembership( + event, + membershipData, + rtcBackendIdentity ?? `${userId}:${deviceId}`, + ); vi.mocked(cms).getTransport = vi.fn().mockReturnValue(fociPreferred[0]); + return cms; } @@ -486,7 +482,7 @@ export class MockRTCSession extends TypedEventEmitter< if (value !== prev) this.emit(MembershipManagerEvent.ProbablyLeft, value); } - public async joinRoomSession(): Promise { + public async joinRTCSession(): Promise { return Promise.resolve(); } } @@ -535,27 +531,3 @@ export function mockMuteStates( const observableScope = new ObservableScope(); return new MuteStates(observableScope, mockMediaDevices({}), joined$); } - -export const mockComputeLivekitParticipantIdentity$ = ( - membership: CallMembershipIdentityParts, - kind: "rtc" | "session", -): Observable => { - function sha256(commitmentStr: string): string { - return encodeUnpaddedBase64( - createHash("sha256").update(commitmentStr, "utf8").digest(), - ); - } - let hash; - switch (kind) { - case "rtc": { - hash = sha256( - `${membership.userId}|${membership.deviceId}|${membership.memberId}`, - ); - break; - } - case "session": - default: - hash = `${membership.userId}:${membership.deviceId}`; - } - return of(hash); -}; From 6480df44e9c4706b6b49ba723b2e70233bb00eea Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 15:10:24 +0100 Subject: [PATCH 30/76] add tests for open id delay fallback --- src/livekit/openIDSFU.test.ts | 99 +++++++++++++++++++ .../MatrixLivekitMembers.test.ts | 16 ++- src/utils/test.ts | 3 +- 3 files changed, 106 insertions(+), 12 deletions(-) diff --git a/src/livekit/openIDSFU.test.ts b/src/livekit/openIDSFU.test.ts index 8b263662..22e487f5 100644 --- a/src/livekit/openIDSFU.test.ts +++ b/src/livekit/openIDSFU.test.ts @@ -34,6 +34,7 @@ describe("getSFUConfigWithOpenID", () => { vitest.clearAllMocks(); fetchMock.reset(); }); + it("should handle fetching a token", async () => { fetchMock.post("https://sfu.example.org/sfu/get", () => { return { @@ -56,6 +57,7 @@ describe("getSFUConfigWithOpenID", () => { }); void (await fetchMock.flush()); }); + it("should fail if the SFU errors", async () => { fetchMock.post("https://sfu.example.org/sfu/get", () => { return { @@ -81,6 +83,103 @@ describe("getSFUConfigWithOpenID", () => { expect.fail("Expected test to throw;"); }); + it("should try legacy and then new endpoint with delay delegation", async () => { + fetchMock.post("https://sfu.example.org/get_token", () => { + return { + status: 500, + body: { error: "Test failure" }, + }; + }); + fetchMock.post("https://sfu.example.org/sfu/get", () => { + return { + status: 500, + body: { error: "Test failure" }, + }; + }); + try { + await getSFUConfigWithOpenID( + matrixClient, + ownMemberMock, + "https://sfu.example.org", + false, + "!example_room_id", + "https://matrix.homeserverserver.org", + "mock_delay_id", + ); + } catch (ex) { + logger.info(ex); + expect(((ex as Error).cause as Error).message).toEqual( + "SFU Config fetch failed with status code 500", + ); + void (await fetchMock.flush()); + } + const calls = fetchMock.calls(); + expect(calls.length).toBe(2); + + expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token"); + expect(calls[0][1]).toStrictEqual({ + // check if it uses correct delayID! + body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":1000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + expect(calls[1][0]).toStrictEqual("https://sfu.example.org/sfu/get"); + + expect(calls[1][1]).toStrictEqual({ + body: '{"room":"!example_room_id","device_id":"DEVICE"}', + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + }); + + it("dont try legacy if endpoint with delay delegation is sucessful", async () => { + fetchMock.post("https://sfu.example.org/get_token", () => { + return { + status: 200, + body: { url: sfuUrl, jwt: testJWTToken }, + }; + }); + fetchMock.post("https://sfu.example.org/sfu/get", () => { + return { + status: 500, + body: { error: "Test failure" }, + }; + }); + try { + await getSFUConfigWithOpenID( + matrixClient, + ownMemberMock, + "https://sfu.example.org", + false, + "!example_room_id", + "https://matrix.homeserverserver.org", + "mock_delay_id", + ); + } catch (ex) { + expect(((ex as Error).cause as Error).message).toEqual( + "SFU Config fetch failed with status code 500", + ); + void (await fetchMock.flush()); + } + const calls = fetchMock.calls(); + expect(calls.length).toBe(1); + + expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token"); + expect(calls[0][1]).toStrictEqual({ + // check if it uses correct delayID! + body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":1000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + }); + it("should retry fetching the openid token", async () => { let count = 0; matrixClient.getOpenIdToken.mockImplementation(async () => { diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts index 55549a10..5d34f7be 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts @@ -49,16 +49,12 @@ const transportB: LivekitTransport = { livekit_alias: "!alias:sample.com", }; -const bobMembership = mockRtcMembership( - "@bob:example.org", - "DEV000", - transportA, -); -const carlMembership = mockRtcMembership( - "@carl:sample.com", - "DEV111", - transportB, -); +const bobMembership = mockRtcMembership("@bob:example.org", "DEV000", { + fociPreferred: [transportA], +}); +const carlMembership = mockRtcMembership("@carl:sample.com", "DEV111", { + fociPreferred: [transportB], +}); beforeEach(() => { testScope = new ObservableScope(); diff --git a/src/utils/test.ts b/src/utils/test.ts index b19ea961..d24ad130 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -15,7 +15,6 @@ import { vitest, } from "vitest"; import { - encodeUnpaddedBase64, MatrixEvent, type Room as MatrixRoom, type Room, @@ -44,7 +43,7 @@ import { type Room as LivekitRoom, Track, } from "livekit-client"; -import { createHash, randomUUID } from "crypto"; +import { randomUUID } from "crypto"; import { type TrackReference } from "@livekit/components-core"; import EventEmitter from "events"; import { From d48042f5220fca828a471892f52ed23879261728 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 15:18:37 +0100 Subject: [PATCH 31/76] fix lint --- src/livekit/openIDSFU.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/livekit/openIDSFU.test.ts b/src/livekit/openIDSFU.test.ts index 22e487f5..aed66d33 100644 --- a/src/livekit/openIDSFU.test.ts +++ b/src/livekit/openIDSFU.test.ts @@ -107,7 +107,6 @@ describe("getSFUConfigWithOpenID", () => { "mock_delay_id", ); } catch (ex) { - logger.info(ex); expect(((ex as Error).cause as Error).message).toEqual( "SFU Config fetch failed with status code 500", ); From dd562bdaf51ec99c63c0a754c31bf2d9d90fe186 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 15:36:32 +0100 Subject: [PATCH 32/76] dont use throw for logic. --- src/livekit/openIDSFU.ts | 51 +++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index b89243c1..6bade4ef 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -105,35 +105,42 @@ export async function getSFUConfigWithOpenID( logger?.info(`Trying to get JWT for focus ${serviceUrl}...`); - let sfuConfig: { url: string; jwt: string }; - try { - // we do not want to try the old endpoint, since we are not sending the new matrix2.0 sticky events (no hashed identity in the event) - if (forceOldJwtEndpoint) throw new Error("Force old jwt endpoint"); - if (!delayId) - throw new Error("No delayId, Won't try matrix 2.0 jwt endpoint."); + let sfuConfig: { url: string; jwt: string } | undefined; - sfuConfig = await getLiveKitJWTWithDelayDelegation( - membership, - serviceUrl, - roomId, - openIdToken, - delayEndpointBaseUrl, - delayId, - ); - logger?.info(`Got JWT from call's active focus URL.`); - } catch (e) { - logger?.warn( - `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy)`, - e, - ); + // If forceOldJwtEndpoint is set we indicate that we do not want to try the new endpoint, + // since we are not sending the new matrix2.0 sticky events (no hashed identity in the event) + if (forceOldJwtEndpoint === false) { + try { + sfuConfig = await getLiveKitJWTWithDelayDelegation( + membership, + serviceUrl, + roomId, + openIdToken, + delayEndpointBaseUrl, + delayId, + ); + logger?.info(`Got JWT from call's active focus URL.`); + } catch (e) { + sfuConfig = undefined; + logger?.warn( + `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy)`, + e, + ); + logger?.info(`Got JWT from call's active focus URL.`); + } + } + + // Either forceOldJwtEndpoint = true or getLiveKitJWTWithDelayDelegation throws -> reset sfuConfig = undefined + if (sfuConfig === undefined) { sfuConfig = await getLiveKitJWT( membership.deviceId, serviceUrl, roomId, openIdToken, ); - logger?.info(`Got JWT from call's active focus URL.`); - } // Pull the details from the JWT + } + + // Pull the details from the JWT const [, payloadStr] = sfuConfig.jwt.split("."); // TODO: Prefer Uint8Array.fromBase64 when widely available const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload; From d814f60f23b8d366bc7ef802ecfd87cbb24536ec Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 15:47:44 +0100 Subject: [PATCH 33/76] review (docstrings) and remove unused artifacts. --- src/state/CallViewModel/CallViewModel.ts | 1 + .../localMember/LocalTransport.ts | 20 +++++++------------ .../remoteMembers/Connection.test.ts | 1 - 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 093abfad..9654920d 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -431,6 +431,7 @@ export function createCallViewModel$( const useOldJwtEndpoint$ = scope.behavior( matrixRTCMode$.pipe(map((v) => v !== MatrixRTCMode.Matrix_2_0)), ); + const localTransport$ = createLocalTransport$({ scope: scope, memberships$: memberships$, diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 0dae3c99..8a6a750b 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -68,11 +68,10 @@ interface Props { * @prop useOldestMember Whether to use the same transport as the oldest member. * This will only update once the first oldest member appears. Will not recompute if the oldest member leaves. * - * @prop useOldJwtEndpoint$ Whether to set forceOldJwtEndpoint the use the old JWT endpoint. + * @prop useOldJwtEndpoint$ Whether to set forceOldJwtEndpoint on the returned transport and to use the old JWT endpoint. * This is used when the connection manager needs to know if it has to use the legacy endpoint which implies a string concatenated rtcBackendIdentity. * (which is expected for non sticky event based rtc member events) - * @returns Behavior<(LivekitTransport & { forceOldJwtEndpoint: boolean }) | null> The `forceOldJwtEndpoint` field is added to let the connection EncryptionManager - * know that this transport is for the local member and it IS RELEVANT which jwt endpoint to use. (for the local member transport, we need to know which jwt endpoint to use) + * @returns The local transport. It will be created using the correct sfu endpoint based on the useOldJwtEndpoint$ value. * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken */ export const createLocalTransport$ = ({ @@ -84,9 +83,7 @@ export const createLocalTransport$ = ({ useOldestMember$, useOldJwtEndpoint$, delayId$, -}: Props): Behavior< - (LivekitTransport & { forceOldJwtEndpoint: boolean }) | null -> => { +}: Props): Behavior => { /** * The transport over which we should be actively publishing our media. * undefined when not joined. @@ -97,7 +94,7 @@ export const createLocalTransport$ = ({ const oldestMember = memberships.value[0]; const transport = oldestMember?.getTransport(memberships.value[0]); if (!transport) return null; - return { ...transport, forceOldJwtEndpoint }; + return transport; }), first((t) => t != null && isLivekitTransport(t)), ), @@ -181,12 +178,10 @@ async function makeTransport( urlFromDevSettings: string | null, forceOldJwtEndpoint: boolean, delayId?: string, -): Promise { +): Promise { logger.trace("Searching for a preferred transport"); - async function doOpenIdAndJWTFromUrl( - url: string, - ): Promise { + async function doOpenIdAndJWTFromUrl(url: string): Promise { const { livekitAlias } = await getSFUConfigWithOpenID( client, membership, @@ -201,7 +196,6 @@ async function makeTransport( type: "livekit", livekit_service_url: url, livekit_alias: livekitAlias, - forceOldJwtEndpoint, }; } // We will call `getSFUConfigWithOpenID` once per transport here as it's our @@ -223,7 +217,7 @@ async function makeTransport( async function getFirstUsableTransport( transports: Transport[], - ): Promise<(LivekitTransport & { forceOldJwtEndpoint: boolean }) | null> { + ): Promise { for (const potentialTransport of transports) { if (isLivekitTransportConfig(potentialTransport)) { try { diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 916e7dd4..239a5c75 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -55,7 +55,6 @@ const livekitFocus: LivekitTransport = { livekit_alias: "!roomID:example.org", livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt", type: "livekit", - useMatrix2: false, }; function setupTest(): void { From 75fca3108a17be45be925e67d86e48d4b5218674 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 16:08:20 +0100 Subject: [PATCH 34/76] cleanup an rename compatibility mode --- src/settings/DeveloperSettingsTab.tsx | 4 ++-- src/settings/settings.ts | 2 +- src/state/CallViewModel/CallViewModel.test.ts | 2 +- src/state/CallViewModel/CallViewModel.ts | 6 +++++- src/state/CallViewModel/localMember/LocalMember.ts | 4 +++- src/state/CallViewModelWidget.test.ts | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 254aaf0f..c88eadf0 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -275,8 +275,8 @@ export const DeveloperSettingsTab: FC = ({ name={matrixRTCModeRadioGroup} control={ } diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 33408fd9..a674f1aa 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -126,7 +126,7 @@ export const alwaysShowIphoneEarpiece = new Setting( export enum MatrixRTCMode { Legacy = "legacy", - Compatibil = "compatibil", + Compatibility = "compatibility", /** This implies using * - sticky events * - hashed RTC backend identity diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 6e3837c4..376d8986 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -235,7 +235,7 @@ const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent; describe.each([ [MatrixRTCMode.Legacy], - [MatrixRTCMode.Compatibil], + [MatrixRTCMode.Compatibility], [MatrixRTCMode.Matrix_2_0], ])("CallViewModel (%s mode)", (mode) => { const withCallViewModel = withCallViewModelInMode(mode); diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 9654920d..c75b3ec4 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -429,7 +429,11 @@ export function createCallViewModel$( }; const useOldJwtEndpoint$ = scope.behavior( - matrixRTCMode$.pipe(map((v) => v !== MatrixRTCMode.Matrix_2_0)), + matrixRTCMode$.pipe( + map( + (v) => v === MatrixRTCMode.Legacy || v === MatrixRTCMode.Compatibility, + ), + ), ); const localTransport$ = createLocalTransport$({ diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 17f766ff..5b72266f 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -711,7 +711,9 @@ export function enterRTCSession( const useDeviceSessionMemberEvents = features?.feature_use_device_session_member_events; const { sendNotificationType: notificationType, callIntent } = getUrlParams(); - const multiSFU = matrixRTCMode !== MatrixRTCMode.Legacy; + const multiSFU = + matrixRTCMode === MatrixRTCMode.Compatibility || + matrixRTCMode === MatrixRTCMode.Matrix_2_0; // Multi-sfu does not need a preferred foci list. just the focus that is actually used. // TODO where/how do we track errors originating from the ongoing rtcSession? rtcSession.joinRTCSession( diff --git a/src/state/CallViewModelWidget.test.ts b/src/state/CallViewModelWidget.test.ts index 5d6442f1..76776720 100644 --- a/src/state/CallViewModelWidget.test.ts +++ b/src/state/CallViewModelWidget.test.ts @@ -37,7 +37,7 @@ vi.mock("../widget", () => ({ it.each([ [MatrixRTCMode.Legacy], - [MatrixRTCMode.Compatibil], + [MatrixRTCMode.Compatibility], [MatrixRTCMode.Matrix_2_0], ])( "expect leave when ElementWidgetActions.HangupCall is called (%s mode)", From d5ad2e38e2583670f4ec11e7a000691b478a536a Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 16:09:08 +0100 Subject: [PATCH 35/76] fix up tests --- src/state/CallViewModel/localMember/LocalTransport.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index c37cab56..2199ca94 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -154,7 +154,6 @@ describe("LocalTransport", () => { await flushPromises(); // final expect(localTransport$.value).toStrictEqual({ - forceOldJwtEndpoint: false, livekit_alias: "!room:example.org", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -196,7 +195,6 @@ describe("LocalTransport", () => { await flushPromises(); // final expect(localTransport$.value).toStrictEqual({ - forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -249,7 +247,6 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ - forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -262,7 +259,6 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ - forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -277,7 +273,6 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ - forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", @@ -309,7 +304,6 @@ describe("LocalTransport", () => { expect(localTransport$.value).toBe(null); await flushPromises(); expect(localTransport$.value).toStrictEqual({ - forceOldJwtEndpoint: false, livekit_alias: "!example_room_id", livekit_service_url: "https://lk.example.org", type: "livekit", From 0eeed4e18e58289b10b21b788281853b758a7938 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 17:02:06 +0100 Subject: [PATCH 36/76] fix test snapshot --- src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap b/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap index 4a2dada0..1c82d07b 100644 --- a/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap +++ b/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap @@ -284,7 +284,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = ` name="_r_0_" title="" type="radio" - value="compatibil" + value="compatibility" />
Date: Wed, 7 Jan 2026 17:21:08 +0100 Subject: [PATCH 37/76] add retries and be more specific when we fall back to legacy endpoint --- src/livekit/openIDSFU.ts | 133 +++++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 54 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 6bade4ef..8c4434bd 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk"; +import { + HTTPError, + retryNetworkOperation, + type IOpenIDToken, + type MatrixClient, +} from "matrix-js-sdk"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type Logger } from "matrix-js-sdk/lib/logger"; @@ -111,33 +116,49 @@ export async function getSFUConfigWithOpenID( // since we are not sending the new matrix2.0 sticky events (no hashed identity in the event) if (forceOldJwtEndpoint === false) { try { - sfuConfig = await getLiveKitJWTWithDelayDelegation( - membership, - serviceUrl, - roomId, - openIdToken, - delayEndpointBaseUrl, - delayId, - ); - logger?.info(`Got JWT from call's active focus URL.`); + await retryNetworkOperation(4, async () => { + sfuConfig = await getLiveKitJWTWithDelayDelegation( + membership, + serviceUrl, + roomId, + openIdToken, + delayEndpointBaseUrl, + delayId, + ); + logger?.info(`Got JWT from call's active focus URL.`); + }); } catch (e) { - sfuConfig = undefined; - logger?.warn( - `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy)`, - e, - ); - logger?.info(`Got JWT from call's active focus URL.`); + if (e instanceof NotSupportedError) { + logger?.warn( + `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported`, + e, + ); + sfuConfig = undefined; + } else { + logger?.warn( + `Failed fetching jwt with matrix 2.0 endpoint other issues -> not going to try with legacy endpoint`, + e, + ); + } } } + // DEPRECATED // Either forceOldJwtEndpoint = true or getLiveKitJWTWithDelayDelegation throws -> reset sfuConfig = undefined if (sfuConfig === undefined) { - sfuConfig = await getLiveKitJWT( - membership.deviceId, - serviceUrl, - roomId, - openIdToken, - ); + await retryNetworkOperation(4, async () => { + sfuConfig = await getLiveKitJWT( + membership.deviceId, + serviceUrl, + roomId, + openIdToken, + ); + }); + logger?.info(`Got JWT from call's active focus URL.`); + } + + if (!sfuConfig) { + throw new Error("No `sfuConfig` after trying with old and new endpoints"); } // Pull the details from the JWT @@ -161,25 +182,28 @@ async function getLiveKitJWT( matrixRoomId: string, openIDToken: IOpenIDToken, ): Promise<{ url: string; jwt: string }> { - try { - const res = await fetch(livekitServiceURL + "/sfu/get", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - // This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used. - room: matrixRoomId, - openid_token: openIDToken, - device_id: deviceId, - }), - }); - if (!res.ok) { - throw new Error("SFU Config fetch failed with status code " + res.status); - } - return await res.json(); - } catch (e) { - throw new Error("SFU Config fetch failed with exception", { cause: e }); + const res = await fetch(livekitServiceURL + "/sfu/get", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + // This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used. + room: matrixRoomId, + openid_token: openIDToken, + device_id: deviceId, + }), + }); + if (!res.ok) { + throw new Error("SFU Config fetch failed with status code " + res.status); + } + return await res.json(); +} + +class NotSupportedError extends Error { + public constructor(message: string) { + super(message); + this.name = "NotSupported"; } } @@ -216,19 +240,20 @@ export async function getLiveKitJWTWithDelayDelegation( }; } - try { - const res = await fetch(livekitServiceURL + "/get_token", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ ...body, ...bodyDalayParts }), - }); - if (!res.ok) { - throw new Error("SFU Config fetch failed with status code " + res.status); + const res = await fetch(livekitServiceURL + "/get_token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...body, ...bodyDalayParts }), + }); + if (!res.ok) { + const msg = "SFU Config fetch failed with status code " + res.status; + if (res.status === 404) { + throw new NotSupportedError(msg); + } else { + throw new Error(msg); } - return await res.json(); - } catch (e) { - throw new Error("SFU Config fetch failed with exception " + e); } + return await res.json(); } From a5a4bb2b8233cebb01cd17aa025ed1b2e97247d3 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 17:38:29 +0100 Subject: [PATCH 38/76] add retries inside the `getLiveKitJWTWithDelayDelegation` and `getLiveKitJWT` functions. --- src/livekit/openIDSFU.ts | 90 +++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 8c4434bd..cf9ad5bc 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -6,7 +6,6 @@ Please see LICENSE in the repository root for full details. */ import { - HTTPError, retryNetworkOperation, type IOpenIDToken, type MatrixClient, @@ -116,17 +115,15 @@ export async function getSFUConfigWithOpenID( // since we are not sending the new matrix2.0 sticky events (no hashed identity in the event) if (forceOldJwtEndpoint === false) { try { - await retryNetworkOperation(4, async () => { - sfuConfig = await getLiveKitJWTWithDelayDelegation( - membership, - serviceUrl, - roomId, - openIdToken, - delayEndpointBaseUrl, - delayId, - ); - logger?.info(`Got JWT from call's active focus URL.`); - }); + sfuConfig = await getLiveKitJWTWithDelayDelegation( + membership, + serviceUrl, + roomId, + openIdToken, + delayEndpointBaseUrl, + delayId, + ); + logger?.info(`Got JWT from call's active focus URL.`); } catch (e) { if (e instanceof NotSupportedError) { logger?.warn( @@ -146,14 +143,13 @@ export async function getSFUConfigWithOpenID( // DEPRECATED // Either forceOldJwtEndpoint = true or getLiveKitJWTWithDelayDelegation throws -> reset sfuConfig = undefined if (sfuConfig === undefined) { - await retryNetworkOperation(4, async () => { - sfuConfig = await getLiveKitJWT( - membership.deviceId, - serviceUrl, - roomId, - openIdToken, - ); - }); + sfuConfig = await getLiveKitJWT( + membership.deviceId, + serviceUrl, + roomId, + openIdToken, + ); + logger?.info(`Got JWT from call's active focus URL.`); } @@ -175,25 +171,33 @@ export async function getSFUConfigWithOpenID( livekitIdentity: payload.sub, }; } - +const RETRIES = 4; async function getLiveKitJWT( deviceId: string, livekitServiceURL: string, matrixRoomId: string, openIDToken: IOpenIDToken, ): Promise<{ url: string; jwt: string }> { - const res = await fetch(livekitServiceURL + "/sfu/get", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - // This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used. - room: matrixRoomId, - openid_token: openIDToken, - device_id: deviceId, - }), + let res: Response | undefined; + await retryNetworkOperation(RETRIES, async () => { + res = await fetch(livekitServiceURL + "/sfu/get", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + // This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used. + room: matrixRoomId, + openid_token: openIDToken, + device_id: deviceId, + }), + }); }); + if (!res) { + throw new Error( + `Network error while connecting to jwt service after ${RETRIES} retries`, + ); + } if (!res.ok) { throw new Error("SFU Config fetch failed with status code " + res.status); } @@ -240,13 +244,23 @@ export async function getLiveKitJWTWithDelayDelegation( }; } - const res = await fetch(livekitServiceURL + "/get_token", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ ...body, ...bodyDalayParts }), + let res: Response | undefined; + + await retryNetworkOperation(RETRIES, async () => { + res = await fetch(livekitServiceURL + "/get_token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...body, ...bodyDalayParts }), + }); }); + + if (!res) { + throw new Error( + `Network error while connecting to jwt service after ${RETRIES} retries`, + ); + } if (!res.ok) { const msg = "SFU Config fetch failed with status code " + res.status; if (res.status === 404) { From 385f63e83e5d0bec8184e62e06e1253d567f8c9a Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Jan 2026 17:46:39 +0100 Subject: [PATCH 39/76] fix tests --- src/livekit/openIDSFU.test.ts | 6 +++--- src/state/CallViewModel/remoteMembers/Connection.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/livekit/openIDSFU.test.ts b/src/livekit/openIDSFU.test.ts index aed66d33..5f286958 100644 --- a/src/livekit/openIDSFU.test.ts +++ b/src/livekit/openIDSFU.test.ts @@ -74,7 +74,7 @@ describe("getSFUConfigWithOpenID", () => { "!example_room_id", ); } catch (ex) { - expect(((ex as Error).cause as Error).message).toEqual( + expect((ex as Error).message).toEqual( "SFU Config fetch failed with status code 500", ); void (await fetchMock.flush()); @@ -107,7 +107,7 @@ describe("getSFUConfigWithOpenID", () => { "mock_delay_id", ); } catch (ex) { - expect(((ex as Error).cause as Error).message).toEqual( + expect((ex as Error).message).toEqual( "SFU Config fetch failed with status code 500", ); void (await fetchMock.flush()); @@ -160,7 +160,7 @@ describe("getSFUConfigWithOpenID", () => { "mock_delay_id", ); } catch (ex) { - expect(((ex as Error).cause as Error).message).toEqual( + expect((ex as Error).message).toEqual( "SFU Config fetch failed with status code 500", ); void (await fetchMock.flush()); diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 239a5c75..0130a5ce 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -259,7 +259,7 @@ describe("Start connection states", () => { capturedState.cause instanceof Error ) { expect(capturedState.cause.message).toContain( - "SFU Config fetch failed with exception", + "SFU Config fetch failed with status code 500", ); expect(connection.transport.livekit_alias).toEqual( livekitFocus.livekit_alias, From 1909aef1862c049482855483a172f15ba8ad0d74 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 8 Jan 2026 12:27:17 +0100 Subject: [PATCH 40/76] temp --- src/livekit/openIDSFU.ts | 5 +++-- src/state/CallViewModel/localMember/LocalMember.ts | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index cf9ad5bc..df1d02a5 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -133,7 +133,8 @@ export async function getSFUConfigWithOpenID( sfuConfig = undefined; } else { logger?.warn( - `Failed fetching jwt with matrix 2.0 endpoint other issues -> not going to try with legacy endpoint`, + `Failed fetching jwt with matrix 2.0 endpoint other issues ->`, + `(not going to try with legacy endpoint: forceOldJwtEndpoint is set to false, we did not get a not supported error from the sfu)`, e, ); } @@ -234,7 +235,7 @@ export async function getLiveKitJWTWithDelayDelegation( let bodyDalayParts = {}; // Also check for empty string - if (delayId && delayEndpointBaseUrl) { + if (delayId && delayEndpointBaseUrl && false) { const delayTimeoutMs = Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms ?? 1000; bodyDalayParts = { diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 5b72266f..eb506132 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -552,7 +552,12 @@ export const createLocalMembership$ = ({ ); const participant$ = scope.behavior( - localConnection$.pipe(map((c) => c?.livekitRoom?.localParticipant ?? null)), + localConnection$.pipe( + map((c) => c?.livekitRoom?.localParticipant ?? null), + tap((p) => { + logger.debug("participant$ updated:", p?.identity); + }), + ), ); // Pause upstream of all local media tracks when we're disconnected from From 0439fdefefd0e5c880b0f8b4555b3a19f1f7112b Mon Sep 17 00:00:00 2001 From: Will Hunt <2072976+Half-Shot@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:51:28 +0000 Subject: [PATCH 41/76] Remove duplicate IntentAndPlatformDerivedConfiguration interface (#3658) --- src/UrlParams.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 048e802b..9b262a43 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -246,10 +246,7 @@ export interface UrlConfiguration { callIntent?: RTCCallIntent; } -interface IntentAndPlatformDerivedConfiguration { - defaultAudioEnabled?: boolean; - defaultVideoEnabled?: boolean; -} + interface IntentAndPlatformDerivedConfiguration { defaultAudioEnabled?: boolean; defaultVideoEnabled?: boolean; From d4b06b0f9c505d33aaa560a2dad2c71deaa2b474 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 8 Jan 2026 14:27:47 +0100 Subject: [PATCH 42/76] fix connection recreation which breaks EC lk connection --- .../remoteMembers/ConnectionManager.ts | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index e5a542df..5c50f0cd 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -110,20 +110,6 @@ export function createConnectionManager$({ const logger = parentLogger.getChild("[ConnectionManager]"); // TODO logger: only construct one logger from the client and make it compatible via a EC specific sing - const allInputTransports$ = combineLatest([ - localTransport$, - remoteTransports$, - ]).pipe( - map(([localTransport, transports]) => { - const localTransportAsArray = localTransport ? [localTransport] : []; - return transports.mapInner((transports) => [ - ...localTransportAsArray, - ...transports, - ]); - }), - map((transports) => transports.mapInner(removeDuplicateTransports)), - ); - /** * All transports currently managed by the ConnectionManager. * @@ -134,30 +120,38 @@ export function createConnectionManager$({ */ const transportsWithJwtTag$ = scope.behavior( combineLatest([ - allInputTransports$, + remoteTransports$, localTransport$, forceOldJwtEndpointForLocalTransport$, ]).pipe( + // combine local and remote transports into one transport array + // and set the forceOldJwtEndpoint property on the local transport map( ([ - transports, + remoteTransports, localTransport, forceOldJwtEndpointForLocalTransport, ]) => { - // modify only the local transport with forceOldJwtEndpointForLocalTransport - const index = transports.value.findIndex((t) => - areLivekitTransportsEqual(localTransport, t), - ); - if (index !== -1) { - transports.value[index].forceOldJwtEndpoint = - forceOldJwtEndpointForLocalTransport; + let localTransportAsArray: (LivekitTransport & { + forceOldJwtEndpoint: boolean; + })[] = []; + if (localTransport) { + localTransportAsArray = [ + { + ...localTransport, + forceOldJwtEndpoint: forceOldJwtEndpointForLocalTransport, + }, + ]; } - logger.trace( - `Managing transports: ${transports.value.map((t) => t.livekit_service_url).join(", ")}`, + return new Epoch( + removeDuplicateTransports([ + ...localTransportAsArray, + ...remoteTransports.value, + ]) as (LivekitTransport & { + forceOldJwtEndpoint?: boolean; + })[], + remoteTransports.epoch, ); - return transports as Epoch< - (LivekitTransport & { forceOldJwtEndpoint?: boolean })[] - >; }, ), ), @@ -181,7 +175,9 @@ export function createConnectionManager$({ }; }, (scope, _data$, serviceUrl, alias, forceOldJwtEndpoint) => { - logger.debug(`Creating connection to ${serviceUrl} (${alias})`); + logger.debug( + `Creating connection to ${serviceUrl} (${alias}, forceOldJwtEndpoint: ${forceOldJwtEndpoint})`, + ); const connection = connectionFactory.createConnection( { type: "livekit", From 8fe49d681adeb2afa4ef24511a1523ee1f0b0be7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:49:47 +0000 Subject: [PATCH 43/76] Update Compound --- yarn.lock | 168 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 150 insertions(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index b350926d..4e7c6e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3838,6 +3838,25 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-arrow@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-arrow@npm:1.1.7" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/c3b46766238b3ee2a394d8806a5141432361bf1425110c9f0dcf480bda4ebd304453a53f294b5399c6ee3ccfcae6fd544921fd01ddc379cf5942acdd7168664b + languageName: node + linkType: hard + "@radix-ui/react-collection@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/react-collection@npm:1.1.1" @@ -3908,16 +3927,16 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-context-menu@npm:^2.2.1": - version: 2.2.4 - resolution: "@radix-ui/react-context-menu@npm:2.2.4" +"@radix-ui/react-context-menu@npm:^2.2.16": + version: 2.2.16 + resolution: "@radix-ui/react-context-menu@npm:2.2.16" dependencies: - "@radix-ui/primitive": "npm:1.1.1" - "@radix-ui/react-context": "npm:1.1.1" - "@radix-ui/react-menu": "npm:2.1.4" - "@radix-ui/react-primitive": "npm:2.0.1" - "@radix-ui/react-use-callback-ref": "npm:1.1.0" - "@radix-ui/react-use-controllable-state": "npm:1.1.0" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-menu": "npm:2.1.16" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -3928,7 +3947,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/f500590b1300dfcd8a2d0fb51fcada0e7d9a1a354ac239328ffdd32f3736bde888ebf0cd64d9039f7d894e3d13eb549a872359669de8c7ff128ee1afb9cf21a8 + checksum: 10c0/950f7559e65474a19145238cf44d744cb1e49be2221ff18436ba49b496b05ccf93bd3906aaa2c7ab76bc77daf694911a78442801e0053f57d2e57ebbfd281c49 languageName: node linkType: hard @@ -4228,6 +4247,42 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-menu@npm:2.1.16": + version: 2.1.16 + resolution: "@radix-ui/react-menu@npm:2.1.16" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-collection": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-focus-guards": "npm:1.1.3" + "@radix-ui/react-focus-scope": "npm:1.1.7" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-roving-focus": "npm:1.1.11" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.6.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/27516b2b987fa9181c4da8645000af8f60691866a349d7a46b9505fa7d2e9d92b9e364db4f7305d08e9e57d0e1afc8df8354f8ee3c12aa05c0100c16b0e76c27 + languageName: node + linkType: hard + "@radix-ui/react-menu@npm:2.1.4": version: 2.1.4 resolution: "@radix-ui/react-menu@npm:2.1.4" @@ -4292,6 +4347,34 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-popper@npm:1.2.8": + version: 1.2.8 + resolution: "@radix-ui/react-popper@npm:1.2.8" + dependencies: + "@floating-ui/react-dom": "npm:^2.0.0" + "@radix-ui/react-arrow": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + "@radix-ui/react-use-rect": "npm:1.1.1" + "@radix-ui/react-use-size": "npm:1.1.1" + "@radix-ui/rect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/48e3f13eac3b8c13aca8ded37d74db17e1bb294da8d69f142ab6b8719a06c3f90051668bed64520bf9f3abdd77b382ce7ce209d056bb56137cecc949b69b421c + languageName: node + linkType: hard + "@radix-ui/react-portal@npm:1.1.3": version: 1.1.3 resolution: "@radix-ui/react-portal@npm:1.1.3" @@ -4476,6 +4559,33 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-roving-focus@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-roving-focus@npm:1.1.11" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-collection": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/2cd43339c36e89a3bf1db8aab34b939113dfbde56bf3a33df2d74757c78c9489b847b1962f1e2441c67e41817d120cb6177943e0f655f47bc1ff8e44fd55b1a2 + languageName: node + linkType: hard + "@radix-ui/react-separator@npm:^1.1.0": version: 1.1.1 resolution: "@radix-ui/react-separator@npm:1.1.1" @@ -4725,6 +4835,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-rect@npm:1.1.1" + dependencies: + "@radix-ui/rect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/271711404c05c589c8dbdaa748749e7daf44bcc6bffc9ecd910821c3ebca0ee245616cf5b39653ce690f53f875c3836fd3f36f51ab1c628273b6db599eee4864 + languageName: node + linkType: hard + "@radix-ui/react-use-size@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-use-size@npm:1.1.0" @@ -4781,6 +4906,13 @@ __metadata: languageName: node linkType: hard +"@radix-ui/rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/rect@npm:1.1.1" + checksum: 10c0/0dac4f0f15691199abe6a0e067821ddd9d0349c0c05f39834e4eafc8403caf724106884035ae91bbc826e10367e6a5672e7bec4d4243860fa7649de246b1f60b + languageName: node + linkType: hard + "@react-spring/animated@npm:~10.0.3": version: 10.0.3 resolution: "@react-spring/animated@npm:10.0.3" @@ -6026,8 +6158,8 @@ __metadata: linkType: hard "@vector-im/compound-design-tokens@npm:^6.0.0": - version: 6.0.0 - resolution: "@vector-im/compound-design-tokens@npm:6.0.0" + version: 6.6.0 + resolution: "@vector-im/compound-design-tokens@npm:6.6.0" peerDependencies: "@types/react": "*" react: ^17 || ^18 || ^19.0.0 @@ -6036,16 +6168,16 @@ __metadata: optional: true react: optional: true - checksum: 10c0/1af5b2b73a3a55149047cd0716f071b83a4df8a210c9ad432db4cc2f9b9e72e958f93ff850dbaddb88e37a01870c5eb810b03dfb0acc89cc147eaaf6cf1dada1 + checksum: 10c0/93b152dd1de96371f9b6b1f7dbcc381d7ab598031dbc900f52d610f015766c0d4426ae6e47d417e723bfb62d1a53099155b4d788848b78232916ba132c03c2fe languageName: node linkType: hard "@vector-im/compound-web@npm:^8.0.0": - version: 8.2.0 - resolution: "@vector-im/compound-web@npm:8.2.0" + version: 8.3.4 + resolution: "@vector-im/compound-web@npm:8.3.4" dependencies: "@floating-ui/react": "npm:^0.27.0" - "@radix-ui/react-context-menu": "npm:^2.2.1" + "@radix-ui/react-context-menu": "npm:^2.2.16" "@radix-ui/react-dropdown-menu": "npm:^2.1.1" "@radix-ui/react-form": "npm:^0.1.0" "@radix-ui/react-progress": "npm:^1.1.0" @@ -6057,12 +6189,12 @@ __metadata: "@fontsource/inconsolata": ^5 "@fontsource/inter": ^5 "@types/react": "*" - "@vector-im/compound-design-tokens": ">=1.6.1 <6.0.0" + "@vector-im/compound-design-tokens": ">=1.6.1 <7.0.0" react: ^18 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/4ac4074dcf9611bdff7de4bf66763397c926d6312f31758bcabe3e7bf704cb76bc2ce1023fe5f2cf0d05e97c9c540fef8b63edea7a521a2f7b4b7fbcb883fb17 + checksum: 10c0/44764fa64b5fce2e7181e25b50ee970eda4d921cf650b92bd5e88df0eb60872f3086b8702d18f55c3e39b3751ac19f10bafda8c4306df65c3605bd44b297d95c languageName: node linkType: hard From f5f8bb549a6944ab14d36811f7045af65bdd258b Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 8 Jan 2026 13:16:58 +0100 Subject: [PATCH 44/76] delete outdated default mute state config --- src/config/ConfigOptions.ts | 8 -------- src/state/MuteStates.ts | 5 ++--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index c587fa50..44cdf128 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -163,10 +163,6 @@ export interface ResolvedConfigOptions extends ConfigOptions { }; }; ssla: string; - media_devices: { - enable_audio: boolean; - enable_video: boolean; - }; app_prompt: boolean; } @@ -181,9 +177,5 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { feature_use_device_session_member_events: true, }, ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf", - media_devices: { - enable_audio: true, - enable_video: true, - }, app_prompt: true, }; diff --git a/src/state/MuteStates.ts b/src/state/MuteStates.ts index 632e0426..7f048f27 100644 --- a/src/state/MuteStates.ts +++ b/src/state/MuteStates.ts @@ -24,7 +24,6 @@ import { import { type MediaDevices, type MediaDevice } from "../state/MediaDevices"; import { ElementWidgetActions, widget } from "../widget"; -import { Config } from "../config/Config"; import { getUrlParams } from "../UrlParams"; import { type ObservableScope } from "./ObservableScope"; import { type Behavior, constant } from "./Behavior"; @@ -192,14 +191,14 @@ export class MuteStates { this.scope, this.mediaDevices.audioInput, this.joined$, - Config.get().media_devices.enable_audio, + true, constant(false), ); public readonly video = new MuteState( this.scope, this.mediaDevices.videoInput, this.joined$, - Config.get().media_devices.enable_video, + true, this.isEarpiece$, ); From a9153f2781dd31bebab2e66cddd6cf6f3f5c99af Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 9 Jan 2026 12:00:45 +0100 Subject: [PATCH 45/76] fix: Regression on default mutestate for voicecall + end-2-end tests --- playwright/fixtures/widget-user.ts | 95 +++++++----- playwright/widget/voice-call-dm.spec.ts | 183 ++++++++++++++++++++++++ sdk/main.ts | 2 +- src/UrlParams.test.ts | 4 - src/UrlParams.ts | 34 +---- src/button/Button.tsx | 3 + src/room/RoomPage.tsx | 15 +- src/state/MuteStates.test.ts | 7 +- src/state/MuteStates.ts | 140 +++++++++--------- src/state/initialMuteState.test.ts | 95 ++++++++++++ src/state/initialMuteState.ts | 46 ++++++ src/utils/test.ts | 2 +- 12 files changed, 467 insertions(+), 159 deletions(-) create mode 100644 playwright/widget/voice-call-dm.spec.ts create mode 100644 src/state/initialMuteState.test.ts create mode 100644 src/state/initialMuteState.ts diff --git a/playwright/fixtures/widget-user.ts b/playwright/fixtures/widget-user.ts index f1f738b7..0611c97e 100644 --- a/playwright/fixtures/widget-user.ts +++ b/playwright/fixtures/widget-user.ts @@ -17,6 +17,7 @@ import type { MatrixClient } from "matrix-js-sdk"; export type UserBaseFixture = { mxId: string; + displayName: string; page: Page; clientHandle: JSHandle; }; @@ -28,6 +29,7 @@ export type BaseWidgetSetup = { export interface MyFixtures { asWidget: BaseWidgetSetup; + callType: "room" | "dm"; } const PASSWORD = "foobarbaz1!"; @@ -145,25 +147,27 @@ async function registerUser( } export const widgetTest = test.extend({ - asWidget: async ({ browser, context }, pUse) => { + // allow per-test override: `widgetTest.use({ callType: "dm" })` + callType: ["room", { option: true }], + asWidget: async ({ browser, context, callType }, pUse) => { await context.route(`http://localhost:8081/config.json*`, async (route) => { await route.fulfill({ json: CONFIG_JSON }); }); - const userA = `brooks_${Date.now()}`; - const userB = `whistler_${Date.now()}`; + const brooksDisplayName = `brooks_${Date.now()}`; + const whistlerDisplayName = `whistler_${Date.now()}`; // Register users const { page: ewPage1, clientHandle: brooksClientHandle, mxId: brooksMxId, - } = await registerUser(browser, userA); + } = await registerUser(browser, brooksDisplayName); const { page: ewPage2, clientHandle: whistlerClientHandle, mxId: whistlerMxId, - } = await registerUser(browser, userB); + } = await registerUser(browser, whistlerDisplayName); // Invite the second user await ewPage1 @@ -171,37 +175,60 @@ export const widgetTest = test.extend({ .getByRole("button", { name: "New conversation" }) .click(); - await ewPage1.getByRole("menuitem", { name: "New Room" }).click(); - await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room"); - await ewPage1.getByRole("button", { name: "Create room" }).click(); - await expect(ewPage1.getByText("You created this room.")).toBeVisible(); - await expect(ewPage1.getByText("Encryption enabled")).toBeVisible(); + if (callType === "room") { - await ewPage1 - .getByRole("button", { name: "Invite to this room", exact: true }) - .click(); - await expect( - ewPage1.getByRole("heading", { name: "Invite to Welcome Room" }), - ).toBeVisible(); + await ewPage1.getByRole("menuitem", { name: "New Room" }).click(); + await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room"); + await ewPage1.getByRole("button", { name: "Create room" }).click(); + await expect(ewPage1.getByText("You created this room.")).toBeVisible(); + await expect(ewPage1.getByText("Encryption enabled")).toBeVisible(); - // To get the invite textbox we need to specifically select within the - // dialog, since there is another textbox in the background (the message - // composer). In theory the composer shouldn't be visible to Playwright at - // all because the invite dialog has trapped focus, but the focus trap - // doesn't quite work right on Firefox. - await ewPage1.getByRole("dialog").getByRole("textbox").fill(whistlerMxId); - await ewPage1.getByRole("dialog").getByRole("textbox").click(); - await ewPage1.getByRole("button", { name: "Invite" }).click(); + await ewPage1 + .getByRole("button", { name: "Invite to this room", exact: true }) + .click(); + await expect( + ewPage1.getByRole("heading", { name: "Invite to Welcome Room" }), + ).toBeVisible(); - // Accept the invite - await expect( - ewPage2.getByRole("option", { name: "Welcome Room" }), - ).toBeVisible(); - await ewPage2.getByRole("option", { name: "Welcome Room" }).click(); - await ewPage2.getByRole("button", { name: "Accept" }).click(); - await expect( - ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }), - ).toBeVisible(); + // To get the invite textbox we need to specifically select within the + // dialog, since there is another textbox in the background (the message + // composer). In theory the composer shouldn't be visible to Playwright at + // all because the invite dialog has trapped focus, but the focus trap + // doesn't quite work right on Firefox. + await ewPage1.getByRole("dialog").getByRole("textbox").fill(whistlerMxId); + await ewPage1.getByRole("dialog").getByRole("textbox").click(); + await ewPage1.getByRole("button", { name: "Invite" }).click(); + + // Accept the invite + await expect( + ewPage2.getByRole("option", { name: "Welcome Room" }), + ).toBeVisible(); + await ewPage2.getByRole("option", { name: "Welcome Room" }).click(); + await ewPage2.getByRole("button", { name: "Accept" }).click(); + await expect( + ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }), + ).toBeVisible(); + } else if (callType === "dm") { + await ewPage1.getByRole("menuitem", { name: "Start chat" }).click(); + await ewPage1.getByRole('textbox', { name: 'Search' }).click(); + await ewPage1.getByRole('textbox', { name: 'Search' }).fill(whistlerMxId); + await ewPage1.getByRole("button", { name: "Go" }).click(); + + // Wait and send the first message to create the DM + await expect(ewPage1.getByText(/Send your first message to invite/)).toBeVisible(); + + await ewPage1.locator('.mx_BasicMessageComposer_input > div').click(); + await ewPage1.getByRole('textbox', { name: 'Send a message…' }).fill('Hello!'); + await ewPage1.getByRole("button", { name: "Send message" }).click(); + + await expect(ewPage1.getByText('This is the beginning of your')).toBeVisible(); + + + // Accept the DM invite from brooks + // This how playwright record selects the DM invite in the room list + await ewPage2.getByRole('option', { name: 'Open room' }).click(); + await ewPage2.getByRole('button', { name: 'Start chatting' }).click(); + } // Renamed use to pUse, as a workaround for eslint error that was thinking this use was a react use. await pUse({ @@ -209,11 +236,13 @@ export const widgetTest = test.extend({ mxId: brooksMxId, page: ewPage1, clientHandle: brooksClientHandle, + displayName: brooksDisplayName }, whistler: { mxId: whistlerMxId, page: ewPage2, clientHandle: whistlerClientHandle, + displayName: whistlerDisplayName }, }); }, diff --git a/playwright/widget/voice-call-dm.spec.ts b/playwright/widget/voice-call-dm.spec.ts new file mode 100644 index 00000000..39a1b8cb --- /dev/null +++ b/playwright/widget/voice-call-dm.spec.ts @@ -0,0 +1,183 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { expect, test } from "@playwright/test"; + +import { widgetTest } from "../fixtures/widget-user.ts"; + +widgetTest.use({callType: "dm"}); + +widgetTest("Start a new voice call in DM as widget", async ({ asWidget }) => { + test.slow(); // Triples the timeout + + const { brooks, whistler } = asWidget; + + await expect( + brooks.page.getByRole("button", { name: "Voice call" }), + ).toBeVisible(); + await brooks.page.getByRole("button", { name: "Voice call" }).click(); + + await expect( + brooks.page.getByRole("menuitem", { name: "Element Call" }), + ).toBeVisible(); + + await brooks.page.getByRole("menuitem", { name: "Element Call" }).click(); + + await expect( + brooks.page + .locator('iframe[title="Element Call"]') + ).toBeVisible(); + + const brooksFrame = brooks.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + + // We should show a ringing overlay, let's check for that + await expect(brooksFrame.getByText(`Waiting for ${whistler.displayName} to join…`)).toBeVisible(); + + + await expect(whistler.page.getByText('Incoming voice call')).toBeVisible(); + await whistler.page.getByRole('button', { name: 'Accept' }).click(); + + await expect( + whistler.page + .locator('iframe[title="Element Call"]') + ).toBeVisible(); + + const whistlerFrame = whistler.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + + // ASSERT the button states for whistler (the callee) + { + // The only way to know if it is muted or not is to look at the data-kind attribute.. + const videoButton = whistlerFrame.getByTestId('incall_videomute'); + // video should be off by default in a voice call + await expect(videoButton).toHaveAttribute("aria-label", /^Start video$/); + + + const audioButton = whistlerFrame.getByTestId('incall_mute'); + // audio should be on for the voice call + await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/); + } + + // ASSERT the button states for brools (the caller) + { + // The only way to know if it is muted or not is to look at the data-kind attribute.. + const videoButton = brooksFrame.getByTestId('incall_videomute'); + // video should be off by default in a voice call + await expect(videoButton).toHaveAttribute("aria-label", /^Start video$/); + + + const audioButton = brooksFrame.getByTestId('incall_mute'); + // audio should be on for the voice call + await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/); + } + + // In order to confirm that the call is disconnected we will check that the message composer is shown again. + // So first we need to confirm that it is hidden when in the call. + await expect(whistler.page.locator(".mx_BasicMessageComposer")).not.toBeVisible(); + await expect(brooks.page.locator(".mx_BasicMessageComposer")).not.toBeVisible(); + + // ASSERT hanging up on one side ends the call for both + { + const hangupButton = brooksFrame.getByTestId('incall_leave'); + await hangupButton.click(); + } + + // The widget should be closed on both sides and the timeline should be back on screen + await expect(whistler.page.locator(".mx_BasicMessageComposer")).toBeVisible(); + await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible(); + +}); + + + +widgetTest("Start a new video call in DM as widget", async ({ asWidget, browserName }) => { + test.slow(); // Triples the timeout + + const { brooks, whistler } = asWidget; + + await expect( + brooks.page.getByRole("button", { name: "Video call" }), + ).toBeVisible(); + await brooks.page.getByRole("button", { name: "Video call" }).click(); + + await expect( + brooks.page.getByRole("menuitem", { name: "Element Call" }), + ).toBeVisible(); + + await brooks.page.getByRole("menuitem", { name: "Element Call" }).click(); + + await expect( + brooks.page + .locator('iframe[title="Element Call"]') + ).toBeVisible(); + + const brooksFrame = brooks.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + + // We should show a ringing overlay, let's check for that + await expect(brooksFrame.getByText(`Waiting for ${whistler.displayName} to join…`)).toBeVisible(); + + + await expect(whistler.page.getByText('Incoming video call')).toBeVisible(); + await whistler.page.getByRole('button', { name: 'Accept' }).click(); + + await expect( + whistler.page + .locator('iframe[title="Element Call"]') + ).toBeVisible(); + + const whistlerFrame = whistler.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + + // ASSERT the button states for whistler (the callee) + { + // The only way to know if it is muted or not is to look at the data-kind attribute.. + const videoButton = whistlerFrame.getByTestId('incall_videomute'); + // video should be on by default in a voice call + await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/); + + + const audioButton = whistlerFrame.getByTestId('incall_mute'); + // audio should be on for the voice call + await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/); + } + + // ASSERT the button states for brools (the caller) + { + // The only way to know if it is muted or not is to look at the data-kind attribute.. + const videoButton = brooksFrame.getByTestId('incall_videomute'); + // video should be on by default in a voice call + await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/); + + + const audioButton = brooksFrame.getByTestId('incall_mute'); + // audio should be on for the voice call + await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/); + } + + // In order to confirm that the call is disconnected we will check that the message composer is shown again. + // So first we need to confirm that it is hidden when in the call. + await expect(whistler.page.locator(".mx_BasicMessageComposer")).not.toBeVisible(); + await expect(brooks.page.locator(".mx_BasicMessageComposer")).not.toBeVisible(); + + // ASSERT hanging up on one side ends the call for both + { + const hangupButton = brooksFrame.getByTestId('incall_leave'); + await hangupButton.click(); + } + + // The widget should be closed on both sides and the timeline should be back on screen + await expect(whistler.page.locator(".mx_BasicMessageComposer")).toBeVisible(); + await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible(); + +}); + diff --git a/sdk/main.ts b/sdk/main.ts index 376674a4..add71dbe 100644 --- a/sdk/main.ts +++ b/sdk/main.ts @@ -99,7 +99,7 @@ export async function createMatrixRTCSdk( if (room === null) throw Error("could not get room from client"); const mediaDevices = new MediaDevices(scope); - const muteStates = new MuteStates(scope, mediaDevices, constant(true)); + const muteStates = new MuteStates(scope, mediaDevices, { audioEnabled: true, videoEnabled: true }); const slot = { application, id }; const rtcSession = new MatrixRTCSession( client, diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index faba394f..bff772c2 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -256,8 +256,6 @@ describe("UrlParams", () => { skipLobby: false, returnToLobby: false, sendNotificationType: "notification", - defaultAudioEnabled: true, - defaultVideoEnabled: true, }); it("use no-intent-defaults with unknown intent", () => { expect(computeUrlParams()).toMatchObject(noIntentDefaults); @@ -395,8 +393,6 @@ describe("UrlParams", () => { expect.any(Object), "configuration:", expect.any(Object), - "intentAndPlatformDerivedConfiguration:", - {}, ); }); }); diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 9b262a43..edac5c07 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -247,11 +247,6 @@ export interface UrlConfiguration { callIntent?: RTCCallIntent; } -interface IntentAndPlatformDerivedConfiguration { - defaultAudioEnabled?: boolean; - defaultVideoEnabled?: boolean; -} - // If you need to add a new flag to this interface, prefer a name that describes // a specific behavior (such as 'confineToRoom'), rather than one that describes // the situations that call for this behavior ('isEmbedded'). This makes it @@ -260,8 +255,7 @@ interface IntentAndPlatformDerivedConfiguration { export interface UrlParams extends UrlProperties, - UrlConfiguration, - IntentAndPlatformDerivedConfiguration {} + UrlConfiguration {} // This is here as a stopgap, but what would be far nicer is a function that // takes a UrlParams and returns a query string. That would enable us to @@ -461,29 +455,6 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => { }; } - const intentAndPlatformDerivedConfiguration: IntentAndPlatformDerivedConfiguration = - {}; - // Desktop also includes web. Its anything that is not mobile. - const desktopMobile = platform === "desktop" ? "desktop" : "mobile"; - switch (desktopMobile) { - case "desktop": - case "mobile": - switch (intent) { - case UserIntent.StartNewCall: - case UserIntent.JoinExistingCall: - case UserIntent.StartNewCallDM: - case UserIntent.JoinExistingCallDM: - intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true; - intentAndPlatformDerivedConfiguration.defaultVideoEnabled = true; - break; - case UserIntent.StartNewCallDMVoice: - case UserIntent.JoinExistingCallDMVoice: - intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true; - intentAndPlatformDerivedConfiguration.defaultVideoEnabled = false; - break; - } - } - const properties: UrlProperties = { widgetId, parentUrl, @@ -548,15 +519,12 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => { properties, "configuration:", configuration, - "intentAndPlatformDerivedConfiguration:", - intentAndPlatformDerivedConfiguration, ); return { ...properties, ...intentPreset, ...pickBy(configuration, (v?: unknown) => v !== undefined), - ...intentAndPlatformDerivedConfiguration, }; }; diff --git a/src/button/Button.tsx b/src/button/Button.tsx index c11c92dd..9cd579d1 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -35,6 +35,7 @@ export const MicButton: FC = ({ muted, ...props }) => { = ({ muted, ...props }) => { > = ({ { + const urlParams = useUrlParams(); const { confineToRoom, appPrompt, preload, header, displayName, skipLobby } = - useUrlParams(); + urlParams; const { t } = useTranslation(); const { roomAlias, roomId, viaServers } = useRoomIdentifier(); @@ -68,15 +68,12 @@ export const RoomPage: FC = () => { const devices = useMediaDevices(); const [muteStates, setMuteStates] = useState(null); - const joined$ = useObservable( - (inputs$) => inputs$.pipe(map(([joined]) => joined)), - [joined], - ); + useEffect(() => { const scope = new ObservableScope(); - setMuteStates(new MuteStates(scope, devices, joined$)); + setMuteStates(new MuteStates(scope, devices, calculateInitialMuteState(urlParams, import.meta.env.VITE_PACKAGE, window.location.hostname))); return (): void => scope.end(); - }, [devices, joined$]); + }, [devices, urlParams]); useEffect(() => { // If we've finished loading, are not already authed and we've been given a display name as diff --git a/src/state/MuteStates.test.ts b/src/state/MuteStates.test.ts index f2a6e35f..db3f503e 100644 --- a/src/state/MuteStates.test.ts +++ b/src/state/MuteStates.test.ts @@ -51,7 +51,6 @@ describe("MuteState", () => { const muteState = new MuteState( testScope, deviceStub, - constant(true), true, forceMute$, ); @@ -166,8 +165,10 @@ describe("MuteStates", () => { const muteStates = new MuteStates( testScope, mediaDevices, - // consider joined - constant(true), + { + audioEnabled: false, + videoEnabled: false, + } ); let latestSyncedState: boolean | null = null; diff --git a/src/state/MuteStates.ts b/src/state/MuteStates.ts index 7f048f27..7b29e31d 100644 --- a/src/state/MuteStates.ts +++ b/src/state/MuteStates.ts @@ -24,7 +24,6 @@ import { import { type MediaDevices, type MediaDevice } from "../state/MediaDevices"; import { ElementWidgetActions, widget } from "../widget"; -import { getUrlParams } from "../UrlParams"; import { type ObservableScope } from "./ObservableScope"; import { type Behavior, constant } from "./Behavior"; @@ -42,12 +41,6 @@ const defaultHandler: Handler = async (desired) => Promise.resolve(desired); * Do not use directly outside of tests. */ export class MuteState { - // TODO: rewrite this to explain behavior, it is not understandable, and cannot add logging - private readonly enabledByDefault$ = - this.enabledByConfig && !getUrlParams().skipLobby - ? this.joined$.pipe(map((isJoined) => !isJoined)) - : of(false); - private readonly handler$ = new BehaviorSubject(defaultHandler); public setHandler(handler: Handler): void { @@ -72,76 +65,73 @@ export class MuteState { private readonly data$ = this.scope.behavior( this.canControlDevices$.pipe( distinctUntilChanged(), - withLatestFrom( - this.enabledByDefault$, - (canControlDevices, enabledByDefault) => { + map((canControlDevices) => { + logger.info( + `MuteState: canControlDevices: ${canControlDevices}, enabled by default: ${this.enabledByDefault}`, + ); + if (!canControlDevices) { logger.info( - `MuteState: canControlDevices: ${canControlDevices}, enabled by default: ${enabledByDefault}`, + `MuteState: devices connected: ${canControlDevices}, disabling`, ); - if (!canControlDevices) { - logger.info( - `MuteState: devices connected: ${canControlDevices}, disabling`, - ); - // We need to sync the mute state with the handler - // to ensure nothing is beeing published. - this.handler$.value(false).catch((err) => { - logger.error("MuteState-disable: handler error", err); - }); - return { enabled$: of(false), set: null, toggle: null }; - } + // We need to sync the mute state with the handler + // to ensure nothing is beeing published. + this.handler$.value(false).catch((err) => { + logger.error("MuteState-disable: handler error", err); + }); + return { enabled$: of(false), set: null, toggle: null }; + } - // Assume the default value only once devices are actually connected - let enabled = enabledByDefault; - const set$ = new Subject(); - const toggle$ = new Subject(); - const desired$ = merge(set$, toggle$.pipe(map(() => !enabled))); - const enabled$ = new Observable((subscriber) => { - subscriber.next(enabled); - let latestDesired = enabledByDefault; - let syncing = false; + // Assume the default value only once devices are actually connected + let enabled = this.enabledByDefault; + const set$ = new Subject(); + const toggle$ = new Subject(); + const desired$ = merge(set$, toggle$.pipe(map(() => !enabled))); + const enabled$ = new Observable((subscriber) => { + subscriber.next(enabled); + let latestDesired = this.enabledByDefault; + let syncing = false; - const sync = async (): Promise => { - if (enabled === latestDesired) syncing = false; - else { - const previouslyEnabled = enabled; - enabled = await firstValueFrom( - this.handler$.pipe( - switchMap(async (handler) => handler(latestDesired)), - ), - ); - if (enabled === previouslyEnabled) { - syncing = false; - } else { - subscriber.next(enabled); - syncing = true; - sync().catch((err) => { - // TODO: better error handling - logger.error("MuteState: handler error", err); - }); - } - } - }; - - const s = desired$.subscribe((desired) => { - latestDesired = desired; - if (syncing === false) { + const sync = async (): Promise => { + if (enabled === latestDesired) syncing = false; + else { + const previouslyEnabled = enabled; + enabled = await firstValueFrom( + this.handler$.pipe( + switchMap(async (handler) => handler(latestDesired)), + ), + ); + if (enabled === previouslyEnabled) { + syncing = false; + } else { + subscriber.next(enabled); syncing = true; sync().catch((err) => { // TODO: better error handling logger.error("MuteState: handler error", err); }); } - }); - return (): void => s.unsubscribe(); - }); - - return { - set: (enabled: boolean): void => set$.next(enabled), - toggle: (): void => toggle$.next(), - enabled$, + } }; - }, - ), + + const s = desired$.subscribe((desired) => { + latestDesired = desired; + if (syncing === false) { + syncing = true; + sync().catch((err) => { + // TODO: better error handling + logger.error("MuteState: handler error", err); + }); + } + }); + return (): void => s.unsubscribe(); + }); + + return { + set: (enabled: boolean): void => set$.next(enabled), + toggle: (): void => toggle$.next(), + enabled$, + }; + }), ), ); @@ -159,8 +149,7 @@ export class MuteState { public constructor( private readonly scope: ObservableScope, private readonly device: MediaDevice, - private readonly joined$: Observable, - private readonly enabledByConfig: boolean, + private readonly enabledByDefault: boolean, /** * An optional observable which, when it emits `true`, will force the mute. * Used for video to stop camera when earpiece mode is on. @@ -175,10 +164,10 @@ export class MuteStates { * True if the selected audio output device is an earpiece. * Used to force-disable video when on earpiece. */ - private readonly isEarpiece$ = combineLatest( + private readonly isEarpiece$ = combineLatest([ this.mediaDevices.audioOutput.available$, this.mediaDevices.audioOutput.selected$, - ).pipe( + ]).pipe( map(([available, selected]) => { if (!selected?.id) return false; const device = available.get(selected.id); @@ -190,22 +179,23 @@ export class MuteStates { public readonly audio = new MuteState( this.scope, this.mediaDevices.audioInput, - this.joined$, - true, + this.initialMuteState.audioEnabled, constant(false), ); public readonly video = new MuteState( this.scope, this.mediaDevices.videoInput, - this.joined$, - true, + this.initialMuteState.videoEnabled, this.isEarpiece$, ); public constructor( private readonly scope: ObservableScope, private readonly mediaDevices: MediaDevices, - private readonly joined$: Observable, + private readonly initialMuteState: { + audioEnabled: boolean; + videoEnabled: boolean; + }, ) { if (widget !== null) { // Sync our mute states with the hosting client diff --git a/src/state/initialMuteState.test.ts b/src/state/initialMuteState.test.ts new file mode 100644 index 00000000..08be50ef --- /dev/null +++ b/src/state/initialMuteState.test.ts @@ -0,0 +1,95 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { test, expect } from "vitest"; +import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc"; + +import { calculateInitialMuteState } from "./initialMuteState"; + + +test.each<{ + callIntent: RTCCallIntent; + packageType: "full" | "embedded"; +}>([ + { callIntent: "audio", packageType: "full" }, + { callIntent: "audio", packageType: "embedded" }, + { callIntent: "video", packageType: "full" }, + { callIntent: "video", packageType: "embedded" }, + { callIntent: "unknown", packageType: "full" }, + { callIntent: "unknown", packageType: "embedded" }, +])( + "Should allow to unmute on start if not skipping lobby (callIntent: $callIntent, packageType: $packageType)", + ({ callIntent, packageType }) => { + const { audioEnabled, videoEnabled } = calculateInitialMuteState( + { skipLobby: false, callIntent }, + packageType, + ); + expect(audioEnabled).toBe(true); + expect(videoEnabled).toBe(callIntent !== "audio"); + }, +); + +test.each<{ + callIntent: RTCCallIntent; +}>([ + { callIntent: "audio" }, + { callIntent: "video" }, + { callIntent: "unknown" }, +])( + "Should always mute on start if skipping lobby on non embedded build (callIntent: $callIntent)", + ({ callIntent }) => { + const { audioEnabled, videoEnabled } = calculateInitialMuteState( + { skipLobby: true, callIntent }, + "full", + ); + expect(audioEnabled).toBe(false); + expect(videoEnabled).toBe(false); + }, +); + +test.each<{ + callIntent: RTCCallIntent; +}>([ + { callIntent: "audio" }, + { callIntent: "video" }, + { callIntent: "unknown" }, +])( + "Can start unmuted if skipping lobby on embedded build (callIntent: $callIntent)", + ({ callIntent }) => { + const { audioEnabled, videoEnabled } = calculateInitialMuteState( + { skipLobby: true, callIntent }, + "embedded", + ); + expect(audioEnabled).toBe(true); + expect(videoEnabled).toBe(callIntent !== "audio"); + }, +); + + +test.each<{ + isDevBuild: boolean; + currentHost: string; + expectedEnabled: boolean; +}>([ + { isDevBuild: true, currentHost: "localhost", expectedEnabled: true }, + { isDevBuild: false, currentHost: "localhost", expectedEnabled: false }, + { isDevBuild: true, currentHost: "call.example.com", expectedEnabled: false }, + { isDevBuild: false, currentHost: "call.example.com", expectedEnabled: false }, +]) +("Should trust localhost domain when in dev mode isDevBuild($isDevBuild) host($currentHost)", ( + {isDevBuild, currentHost, expectedEnabled} +) => { + const { audioEnabled, videoEnabled } = calculateInitialMuteState( + { skipLobby: true, callIntent: "video" }, + "full", + currentHost, + isDevBuild, + ); + + expect(audioEnabled).toBe(expectedEnabled); + expect(videoEnabled).toBe(expectedEnabled); +}); diff --git a/src/state/initialMuteState.ts b/src/state/initialMuteState.ts new file mode 100644 index 00000000..41b36559 --- /dev/null +++ b/src/state/initialMuteState.ts @@ -0,0 +1,46 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type UrlParams } from "../UrlParams.ts"; + +/** + * Calculates the initial mute state for media devices based on configuration. + * + * It is not always possible to start the widget with audio/video unmuted due to privacy concerns. + * This function encapsulates the logic to determine the appropriate initial state. + */ +export function calculateInitialMuteState( + urlParams: Pick, + packageType: "full" | "embedded", + hostname: string | undefined = undefined, + isDevBuild: boolean = import.meta.env.DEV, +): { audioEnabled: boolean; videoEnabled: boolean } { + const { skipLobby, callIntent } = urlParams; + + const isTrustedHost = + packageType == "embedded" || + // Trust local hosts in dev mode to make local testing easier + (hostname == "localhost" && isDevBuild); + + if (skipLobby && !isTrustedHost) { + // If host not trusted and lobby skipped, default to muted to protect user privacy. + // This prevents users from inadvertently joining with active audio/video + // when browser permissions were previously granted in a different context. + return { + audioEnabled: false, + videoEnabled: false, + }; + } + + // Embedded contexts are trusted environments, so they allow unmuted by default. + // Same for when showing a lobby, as users can adjust their settings there. + // Additionally, if the call intent is "audio", we disable video by default. + return { + audioEnabled: true, + videoEnabled: callIntent != "audio", + }; +} diff --git a/src/utils/test.ts b/src/utils/test.ts index 9a845908..95d6ed0c 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -525,5 +525,5 @@ export function mockMuteStates( joined$: Observable = of(true), ): MuteStates { const observableScope = new ObservableScope(); - return new MuteStates(observableScope, mockMediaDevices({}), joined$); + return new MuteStates(observableScope, mockMediaDevices({}), { audioEnabled: false, videoEnabled: false }); } From 231a80d9de13ec9ca2be35b082540dba68dca0da Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 9 Jan 2026 12:32:36 +0100 Subject: [PATCH 46/76] update snapshot, mute buttons have aria-label now --- src/room/__snapshots__/InCallView.test.tsx.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index a6405579..52f4a702 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -286,6 +286,7 @@ exports[`InCallView > rendering > renders 1`] = ` >