From 8f38ecac2efc82036b3b0ab58bf85ff46ddfe252 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 26 Mar 2026 15:29:51 +0100 Subject: [PATCH 01/33] msc4039 support b64 in addition to blob file download --- src/Avatar.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Avatar.tsx b/src/Avatar.tsx index d0cb243c..46e2bf21 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -181,9 +181,16 @@ async function getAvatarFromWidgetAPI( const file = response.file; // element-web sends a Blob, and the MSC4039 is considering changing the spec to strictly Blob, so only handling that - if (!(file instanceof Blob)) { - throw new Error("Downloaded file is not a Blob"); + if (file instanceof Blob) { + return file; + } else if (typeof file === "string") { + // it is a base64 string + const bytes = Uint8Array.from(atob(file), (c) => + c.charCodeAt(0), + ); + return new Blob([bytes]); } + throw new Error("Downloaded file format is not supported: " + typeof file + ""); + - return file; } From 1eabb0f0aaeb29135e3fbddcf16f5ccc6d1d405b Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 26 Mar 2026 18:14:51 +0100 Subject: [PATCH 02/33] add tests --- src/Avatar.test.tsx | 36 +++++++++++++++++++++++++++++++++++- src/Avatar.tsx | 13 ++++++------- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx index 25d2c42b..cf00cbbb 100644 --- a/src/Avatar.test.tsx +++ b/src/Avatar.test.tsx @@ -12,7 +12,7 @@ import { type FC, type PropsWithChildren } from "react"; import { type WidgetApi } from "matrix-widget-api"; import { ClientContextProvider } from "./ClientContext"; -import { Avatar } from "./Avatar"; +import { Avatar, getAvatarFromWidgetAPI } from "./Avatar"; import { mockMatrixRoomMember, mockRtcMembership } from "./utils/test"; import { widget } from "./widget"; @@ -178,3 +178,37 @@ test("should attempt to use widget API if running as a widget", async () => { expect(widget!.api.downloadFile).toBeCalledWith(expectedMXCUrl); }); + +test("Supports download files as base64", async () => { + const expectedMXCUrl = "mxc://example.org/alice-avatar"; + const expectedBase64 = + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAADIElEQVR4nAAQA+/8ApxhEfFNuwna" + + "+DO1pFMx5YDg6gb8p1WFkbFSox9H6r5c8jp1gxlHXrDfA/oQFi4A0gTXH9YBNgwRm12xO68QP6lv" + + "ZLKH9qW1VM6kz6zA3T1Ui8J+Xbnh2BZ7oXDe/2gajzoA6j1JGotpz99xO+T2NR634Nhx3zhuera/" + + "UdrpMLdEpwWXLnSqZRasGsrl93FjdTwRBMaqsx6vJksnPOmV9ttbXFIOb0XDGPbVythSC2n7P/bS" + + "Zv0U0QqbBLk/5Wu1werYzAHiz11Bj8bEylQ92Pxvo+PwF6/KbGnIHTvGZkFzDkMnqz3g7Pw3NOSP" + + "oV+qfyJuSI0AeZmrPejFQ8kzBSDWO8D7lr4+6ePRBRmZtKCf+fNjSCOyb5jqwhBnD2cycbJtQQbR" + + "A4qdPG2ONfTPeQgi96+zT7grBI0JwvgFBceJdLJd4BX1VQIyY+j7OYueNWqEpf8iYgMj78I95eRt" + + "nfPLwlxhVns84iL4Yvw8jDrB9vQi8ktpsdJOMiDwKrBGD3q56COD2oIA96CCBgiro4tkvkumZSAc" + + "ZKXRLsziUFGytWJLaPjwnzXv2hicPy6k9AXsF3QkysOZAkB3m9XPpixhq9b0OKqV/zZx3L79o6wZ" + + "Dr40J7sj7f+ARd545CP01r5omHt94tbnjgA46HsM2OhP+qQ882LN+Bhscq2WSHGSHT4J9MQcsWZP" + + "2+N2LdPy61MN4/1++BJHmDcDLQBUEwLvjZp1fRfzxV7yirwIiOA7Vr8z+1yvS/pSkfUzkjswybOd" + + "M5i0I8Q69MTXAKxqtR0/tyGkfCmHfupGASp/SAT9J8f3aQV+gDbpva592v4w8Cv5EMm7CzZPwThF" + + "kgTChNPts7F03ccxpblfIz0EiAON1DKk71rX07BvDlLHY1ItPuqZ7hjy19jrAgl+QqEE1btHVA5R" + + "uAnRXpEWc6rjARlJY5G1wbMk12rrqpr8rhR3YpFgLgOx4BtQ0D/hGe7KANSGBMQojmObId0asCmd" + + "XzmnQI9P8QnwsO9vtqZlgIoU4g+f2/G8Q3/nVMX7dujniwEAAP//KmiQs7P8MeIAAAAASUVORK5C" + + "YII="; + const mockWidgetAPI = { + downloadFile: vi.fn().mockImplementation( + (contentUri) => { + if (contentUri !== expectedMXCUrl) { + return Promise.reject(new Error("Unexpected content URI")); + } + return {file: expectedBase64 } + }), + } as unknown as WidgetApi; + + const blob = await getAvatarFromWidgetAPI(mockWidgetAPI, expectedMXCUrl); + + expect(blob).toBeInstanceOf(Blob); +}); diff --git a/src/Avatar.tsx b/src/Avatar.tsx index 46e2bf21..99940540 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -173,7 +173,8 @@ async function getAvatarFromServer( return blob; } -async function getAvatarFromWidgetAPI( +// export for testing +export async function getAvatarFromWidgetAPI( api: WidgetApi, src: string, ): Promise { @@ -185,12 +186,10 @@ async function getAvatarFromWidgetAPI( return file; } else if (typeof file === "string") { // it is a base64 string - const bytes = Uint8Array.from(atob(file), (c) => - c.charCodeAt(0), - ); + const bytes = Uint8Array.from(atob(file), (c) => c.charCodeAt(0)); return new Blob([bytes]); } - throw new Error("Downloaded file format is not supported: " + typeof file + ""); - - + throw new Error( + "Downloaded file format is not supported: " + typeof file + "", + ); } From 421c49e7ba600c9d9632ed8ae0eb65e076494ec9 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 26 Mar 2026 19:10:10 +0100 Subject: [PATCH 03/33] fixup prettier --- src/Avatar.test.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx index cf00cbbb..1816904c 100644 --- a/src/Avatar.test.tsx +++ b/src/Avatar.test.tsx @@ -199,13 +199,12 @@ test("Supports download files as base64", async () => { "XzmnQI9P8QnwsO9vtqZlgIoU4g+f2/G8Q3/nVMX7dujniwEAAP//KmiQs7P8MeIAAAAASUVORK5C" + "YII="; const mockWidgetAPI = { - downloadFile: vi.fn().mockImplementation( - (contentUri) => { - if (contentUri !== expectedMXCUrl) { - return Promise.reject(new Error("Unexpected content URI")); - } - return {file: expectedBase64 } - }), + downloadFile: vi.fn().mockImplementation((contentUri) => { + if (contentUri !== expectedMXCUrl) { + return Promise.reject(new Error("Unexpected content URI")); + } + return { file: expectedBase64 }; + }), } as unknown as WidgetApi; const blob = await getAvatarFromWidgetAPI(mockWidgetAPI, expectedMXCUrl); From 3389fb5a34b6b59a4638fecece7be363c31029e7 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 26 Mar 2026 19:39:02 +0100 Subject: [PATCH 04/33] fixup linter --- src/Avatar.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx index 1816904c..1e32de0e 100644 --- a/src/Avatar.test.tsx +++ b/src/Avatar.test.tsx @@ -199,7 +199,7 @@ test("Supports download files as base64", async () => { "XzmnQI9P8QnwsO9vtqZlgIoU4g+f2/G8Q3/nVMX7dujniwEAAP//KmiQs7P8MeIAAAAASUVORK5C" + "YII="; const mockWidgetAPI = { - downloadFile: vi.fn().mockImplementation((contentUri) => { + downloadFile: vi.fn().mockImplementation(async (contentUri) => { if (contentUri !== expectedMXCUrl) { return Promise.reject(new Error("Unexpected content URI")); } From 6b7467ce6dc3a1dea74b3a99e1362d8d87fcf8c5 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 31 Mar 2026 11:38:21 +0200 Subject: [PATCH 05/33] Stop calling rtc/transport in widget mode --- src/livekit/openIDSFU.ts | 1 + .../localMember/LocalTransport.test.ts | 51 +++++++++++++++++++ .../localMember/LocalTransport.ts | 41 ++++++++++++--- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 8360cdc7..d3756e6c 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -137,6 +137,7 @@ export async function getSFUConfigWithOpenID( ); logger?.info(`Got JWT from call's active focus URL.`); } catch (e) { + logger?.debug(`Failed fetching jwt with matrix 2.0 endpoint:`, e); if (e instanceof NotSupportedError) { logger?.warn( `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported`, diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 8454b09a..27789ba8 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -21,6 +21,7 @@ import { } from "matrix-js-sdk/lib/matrixrtc"; import { BehaviorSubject, lastValueFrom } from "rxjs"; import fetchMock from "fetch-mock"; +import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { mockConfig, @@ -60,6 +61,7 @@ describe("LocalTransport", () => { client: { // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: async () => Promise.resolve([]), + getAccessToken: vi.fn().mockReturnValue("access_token"), getDomain: () => "", baseUrl: "example.org", // These won't be called in this error path but satisfy the type @@ -102,6 +104,7 @@ describe("LocalTransport", () => { baseUrl: "https://lk.example.org", // Use empty domain to skip .well-known and use config directly getDomain: () => "", + getAccessToken: vi.fn().mockReturnValue("access_token"), // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: async () => Promise.resolve([]), getOpenIdToken: vi.fn(), @@ -149,6 +152,7 @@ describe("LocalTransport", () => { getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), baseUrl: "https://lk.example.org", + getAccessToken: vi.fn().mockReturnValue("access_token"), }, ownMembershipIdentity: ownMemberMock, forceJwtEndpoint: JwtEndpointVersion.Legacy, @@ -217,6 +221,7 @@ describe("LocalTransport", () => { getDomain: () => "", // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: async () => Promise.resolve([]), + getAccessToken: vi.fn().mockReturnValue("access_token"), getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), baseUrl: "https://lk.example.org", @@ -273,6 +278,7 @@ describe("LocalTransport", () => { // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: async () => Promise.resolve([aliceTransport]), + getAccessToken: vi.fn().mockReturnValue("access_token"), getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), baseUrl: "https://lk.example.org", @@ -323,6 +329,7 @@ describe("LocalTransport", () => { getDomain: vi.fn().mockReturnValue(""), // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), + getAccessToken: vi.fn().mockReturnValue("access_token"), getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), }, @@ -410,6 +417,49 @@ describe("LocalTransport", () => { }); }); + it("Should not call _unstable_getRTCTransports in widget mode but use well-known", async () => { + mockConfig({ + livekit: { livekit_service_url: "https://do-not-use.lk.example.org" }, + }); + + localTransportOpts.client.getDomain.mockReturnValue("example.org"); + + vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation( + async (domain) => { + if (domain === "example.org") { + return Promise.resolve({ + "org.matrix.msc4143.rtc_foci": [ + { + type: "livekit", + livekit_service_url: "https://use-me.jwt.call.example.org", + }, + ], + }); + } + return Promise.resolve({}); + }, + ); + + localTransportOpts.client.getAccessToken.mockReturnValue(null); + const { advertised$, active$ } = + createLocalTransport$(localTransportOpts); + openIdResolver.resolve?.(openIdResponse); + expect(advertised$.value).toBe(null); + expect(active$.value).toBe(null); + await flushPromises(); + + expect( + localTransportOpts.client._unstable_getRTCTransports, + ).not.toHaveBeenCalled(); + + const expectedTransport = { + type: "livekit", + livekit_service_url: "https://use-me.jwt.call.example.org", + }; + + expect(advertised$.value).toStrictEqual(expectedTransport); + }); + it("fails fast if the openID request fails for backend config", async () => { localTransportOpts.client._unstable_getRTCTransports.mockResolvedValue([ { type: "livekit", livekit_service_url: "https://lk.example.org" }, @@ -481,6 +531,7 @@ describe("LocalTransport", () => { baseUrl: "https://example.org", // eslint-disable-next-line @typescript-eslint/naming-convention _unstable_getRTCTransports: async () => Promise.resolve([]), + getAccessToken: vi.fn().mockReturnValue("access_token"), // These won't be called in this error path but satisfy the type getOpenIdToken: vi.fn(), getDeviceId: vi.fn(), diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 0b566ba0..da4fe1dc 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -56,7 +56,7 @@ interface Props { memberships$: Behavior>; client: Pick< MatrixClient, - "getDomain" | "baseUrl" | "_unstable_getRTCTransports" + "getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken" > & OpenIDClientParts; // Used by the jwt service to create the livekit room and compute the livekit alias. @@ -314,7 +314,7 @@ const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; async function makeTransport( client: Pick< MatrixClient, - "getDomain" | "baseUrl" | "_unstable_getRTCTransports" + "getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken" > & OpenIDClientParts, membership: CallMembershipIdentityParts, @@ -371,11 +371,18 @@ async function makeTransport( for (const potentialTransport of transports) { if (isLivekitTransportConfig(potentialTransport)) { try { + logger.info( + `makeTransport: check transport authentication for "${potentialTransport.livekit_service_url}"`, + ); // This will call the jwt/sfu/get endpoint to pre create the livekit room. return await doOpenIdAndJWTFromUrl( potentialTransport.livekit_service_url, ); } catch (ex) { + logger.debug( + `makeTransport: Could not use SFU service "${potentialTransport.livekit_service_url}" as SFU`, + ex, + ); // Explictly throw these if (ex instanceof FailToGetOpenIdToken) { throw ex; @@ -383,24 +390,34 @@ async function makeTransport( if (ex instanceof NoMatrix2AuthorizationService) { throw ex; } - logger.debug( - `Could not use SFU service "${potentialTransport.livekit_service_url}" as SFU`, - ex, - ); } + } else { + logger.info( + `makeTransport: "${potentialTransport.livekit_service_url}" is not a valid livekit transport as SFU`, + ); } } return null; } // MSC4143: Attempt to fetch transports from backend. - if ("_unstable_getRTCTransports" in client) { + // TODO: Workaround for an issue in the js-sdk RoomWidgetClient that + // is not yet implementing _unstable_getRTCTransports properly (via widget API new action). + // For now we just skip this call if we are in a widget. + // In widget mode the client is a `RoomWidgetClient` which has no access token (it is using the widget API). + // Could be removed once the js-sdk is fixed (https://github.com/matrix-org/matrix-js-sdk/issues/5245) + const isSPA = !!client.getAccessToken(); + if (isSPA && "_unstable_getRTCTransports" in client) { + logger.info( + "makeTransport: First try to use getRTCTransports end point ...", + ); try { + // TODO This should also check for server support? const transportList = await client._unstable_getRTCTransports(); const selectedTransport = await getFirstUsableTransport(transportList); if (selectedTransport) { logger.info( - "Using backend-configured (client.getRTCTransports) SFU", + "makeTransport: ...Using backend-configured (client.getRTCTransports) SFU", selectedTransport, ); return selectedTransport; @@ -424,6 +441,10 @@ async function makeTransport( } } + logger.info( + `makeTransport: Trying to get transports from .well-known/matrix/client on domain ${client.getDomain()} ...`, + ); + // Legacy MSC4143 (to be removed) WELL_KNOWN: Prioritize the .well-known/matrix/client, if available. const domain = client.getDomain(); if (domain) { @@ -441,6 +462,10 @@ async function makeTransport( } } + logger.info( + `makeTransport: No valid transport found via backend or .well-known, falling back to config if available.`, + ); + // CONFIG: Least prioritized; Load from config file const urlFromConf = Config.get().livekit?.livekit_service_url; if (urlFromConf) { From 8cee4df46e08557bf87c84cef3c4c0742ddc840e Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 31 Mar 2026 11:55:49 +0200 Subject: [PATCH 06/33] fix test: use fetchMock to avoid test interference --- .../localMember/LocalTransport.test.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 27789ba8..f6c00c7f 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -21,7 +21,6 @@ import { } from "matrix-js-sdk/lib/matrixrtc"; import { BehaviorSubject, lastValueFrom } from "rxjs"; import fetchMock from "fetch-mock"; -import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { mockConfig, @@ -424,21 +423,14 @@ describe("LocalTransport", () => { localTransportOpts.client.getDomain.mockReturnValue("example.org"); - vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation( - async (domain) => { - if (domain === "example.org") { - return Promise.resolve({ - "org.matrix.msc4143.rtc_foci": [ - { - type: "livekit", - livekit_service_url: "https://use-me.jwt.call.example.org", - }, - ], - }); - } - return Promise.resolve({}); - }, - ); + fetchMock.getOnce("https://example.org/.well-known/matrix/client", { + "org.matrix.msc4143.rtc_foci": [ + { + type: "livekit", + livekit_service_url: "https://use-me.jwt.call.example.org", + }, + ], + }); localTransportOpts.client.getAccessToken.mockReturnValue(null); const { advertised$, active$ } = From 0dabb43e0fc4d619304a7d9ce4c554e0ddcdd833 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 31 Mar 2026 14:56:25 +0200 Subject: [PATCH 07/33] Workaround: EW has not yet merged the download file permission for EC --- playwright/widget/test-helpers.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 630681bd..38d34304 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -34,6 +34,9 @@ export class TestHelpers { ).toBeVisible(); await page.getByRole("menuitem", { name: "Element Call" }).click(); + + // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 + await this.dismissFileDialogPermissionIfNeeded(page); } public static async joinCallFromLobby(page: Page): Promise { @@ -60,6 +63,9 @@ export class TestHelpers { await expect(page.getByText(label)).toBeVisible(); await expect(page.getByRole("button", { name: "Join" })).toBeVisible(); await page.getByRole("button", { name: "Join" }).click(); + + // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 + await this.dismissFileDialogPermissionIfNeeded(page); } /** @@ -235,9 +241,30 @@ export class TestHelpers { ): Promise { await page.getByRole("button", { name: "Video call" }).click(); await page.getByRole("menuitem", { name: "Element Call" }).click(); + + // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 + await this.dismissFileDialogPermissionIfNeeded(page); + await TestHelpers.setEmbeddedElementCallRtcMode(page, mode); await page.getByRole("button", { name: "Close lobby" }).click(); } + + // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 + private static async dismissFileDialogPermissionIfNeeded( + page: Page, + ): Promise { + const dialogHeading = page.getByRole("heading", { + name: "Approve widget permissions", + }); + + try { + await expect(dialogHeading).toBeVisible({ timeout: 3000 }); + await page.getByRole("button", { name: "Approve" }).click(); + } catch { + // Dialog did not appear, that's fine + } + } + /** * Goes to the settings to set the RTC mode. * then closes the settings modal. From c9242148f99218da2a13fa3fcfd43db884a6ef11 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 31 Mar 2026 15:24:12 +0200 Subject: [PATCH 08/33] add missing dismiss dialog test --- playwright/widget/test-helpers.ts | 2 +- playwright/widget/voice-call-dm.spec.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 38d34304..31247e1a 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -250,7 +250,7 @@ export class TestHelpers { } // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 - private static async dismissFileDialogPermissionIfNeeded( + public static async dismissFileDialogPermissionIfNeeded( page: Page, ): Promise { const dialogHeading = page.getByRole("heading", { diff --git a/playwright/widget/voice-call-dm.spec.ts b/playwright/widget/voice-call-dm.spec.ts index a4e6255b..f5152e1f 100644 --- a/playwright/widget/voice-call-dm.spec.ts +++ b/playwright/widget/voice-call-dm.spec.ts @@ -45,6 +45,8 @@ widgetTest( await expect(whistler.page.getByText("Incoming voice call")).toBeVisible(); await whistler.page.getByRole("button", { name: "Accept" }).click(); + await TestHelpers.dismissFileDialogPermissionIfNeeded(whistler.page); + await expect( whistler.page.locator('iframe[title="Element Call"]'), ).toBeVisible(); @@ -138,6 +140,8 @@ widgetTest( await expect(whistler.page.getByText("Incoming video call")).toBeVisible(); await whistler.page.getByRole("button", { name: "Accept" }).click(); + await TestHelpers.dismissFileDialogPermissionIfNeeded(whistler.page); + await expect( whistler.page.locator('iframe[title="Element Call"]'), ).toBeVisible(); From d5ec3c342c6bd08c2bbe6453de171bce1963e7ff Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 31 Mar 2026 16:06:56 +0200 Subject: [PATCH 09/33] reuse exisiting helpers to join call --- playwright/widget/simple-create.spec.ts | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/playwright/widget/simple-create.spec.ts b/playwright/widget/simple-create.spec.ts index 8c889892..4686e99d 100644 --- a/playwright/widget/simple-create.spec.ts +++ b/playwright/widget/simple-create.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "@playwright/test"; import { widgetTest } from "../fixtures/widget-user.ts"; +import { TestHelpers } from "./test-helpers.ts"; // Skip test, including Fixtures widgetTest.skip( @@ -20,19 +21,7 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { 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: "Legacy Call" }), - ).toBeVisible(); - await expect( - brooks.page.getByRole("menuitem", { name: "Element Call" }), - ).toBeVisible(); - - await brooks.page.getByRole("menuitem", { name: "Element Call" }).click(); + await TestHelpers.startCallInCurrentRoom(brooks.page, false); await expect( brooks.page @@ -56,11 +45,7 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { ).toBeVisible(); // Join from the other side - await expect(whistler.page.getByText("Video call started")).toBeVisible(); - await expect( - whistler.page.getByRole("button", { name: "Join" }), - ).toBeVisible(); - await whistler.page.getByRole("button", { name: "Join" }).click(); + await TestHelpers.joinCallInCurrentRoom(whistler.page); // Currently disabled due to recent Element Web is bypassing Lobby // await expect( From f7488a0474d1c0e963971dba5256fd8787e5c9fa Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 1 Apr 2026 13:16:39 +0200 Subject: [PATCH 10/33] Avoid redundantly showing the local user in the PiP tile If you are the only participant in the call, the expanded spotlight layout would redundantly show your media in both the spotlight and PiP tiles. This is a regression; in versions 0.16.1 and earlier we would avoid showing the same user twice. --- src/state/CallViewModel/CallViewModel.test.ts | 47 +++++++++++++++++++ src/state/CallViewModel/CallViewModel.ts | 7 ++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index aca3ee7b..1fecd112 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -750,6 +750,53 @@ describe.each([ }); }); + test("PiP tile in expanded spotlight layout avoids redundantly showing local user", () => { + withTestScheduler(({ behavior, schedule, expectObservable }) => { + // Switch to spotlight immediately + const modeInputMarbles = " s"; + // And expand the spotlight immediately + const expandInputMarbles = " a"; + // First no one else is in the call, then Alice joins + const participantInputMarbles = "ab"; + // First local user should be in the spotlight, then they appear in PiP + // only once Alice has joined + const expectedLayoutMarbles = " ab"; + + withCallViewModel( + { + rtcMembers$: behavior(participantInputMarbles, { + a: [localRtcMember], + b: [localRtcMember, aliceRtcMember], + }), + }, + (vm) => { + schedule(modeInputMarbles, { + s: () => vm.setGridMode("spotlight"), + }); + schedule(expandInputMarbles, { + a: () => vm.toggleSpotlightExpanded$.value!(), + }); + + expectObservable(summarizeLayout$(vm.layout$)).toBe( + expectedLayoutMarbles, + { + a: { + type: "spotlight-expanded", + spotlight: [`${localId}:0`], + pip: undefined, + }, + b: { + type: "spotlight-expanded", + spotlight: [`${aliceId}:0`], + pip: `${localId}:0`, + }, + }, + ); + }, + ); + }); + }); + test("spotlight remembers whether it's expanded", () => { withTestScheduler(({ schedule, expectObservable }) => { // Start in spotlight mode, then switch to grid and back to spotlight a diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 8b4d19fb..6dca08dc 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -951,7 +951,7 @@ export function createCallViewModel$( const spotlightAndPip$ = scope.behavior<{ spotlight: MediaViewModel[]; - pip$: Behavior; + pip$: Observable; }>( ringingMedia$.pipe( switchMap((ringingMedia) => { @@ -966,7 +966,10 @@ export function createCallViewModel$( return spotlightSpeaker$.pipe( map((speaker) => ({ spotlight: speaker ? [speaker] : [], - pip$: localUserMediaForPip$, + // Hide PiP if redundant (i.e. if local user is already in spotlight) + pip$: localUserMediaForPip$.pipe( + map((m) => (m === speaker ? undefined : m)), + ), })), ); }), From 99ba23bc96f0831d431dce34dbeb9fc3998e2c15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:33:25 +0200 Subject: [PATCH 11/33] Update GitHub Actions (#3831) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/publish-embedded-packages.yaml | 4 ++-- .github/workflows/publish.yaml | 4 ++-- .github/workflows/test.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-embedded-packages.yaml b/.github/workflows/publish-embedded-packages.yaml index 3ab275a5..88d59947 100644 --- a/.github/workflows/publish-embedded-packages.yaml +++ b/.github/workflows/publish-embedded-packages.yaml @@ -97,7 +97,7 @@ jobs: run: find ${FILENAME_PREFIX} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${FILENAME_PREFIX}.sha256 - name: Upload if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }} - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 with: files: | ${{ env.FILENAME_PREFIX }}.tar.gz @@ -297,7 +297,7 @@ jobs: NEEDS_PUBLISH_IOS_OUTPUTS_ARTIFACT_VERSION: ${{ needs.publish_ios.outputs.ARTIFACT_VERSION }} - name: Add release notes if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }} - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 with: append_body: true body: | diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0675b1b1..40b94e26 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -42,7 +42,7 @@ jobs: - name: Create Checksum run: find ${FILENAME_PREFIX} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${FILENAME_PREFIX}.sha256 - name: Upload - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 with: files: | ${{ env.FILENAME_PREFIX }}.tar.gz @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Add release note - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 with: append_body: true body: | diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cd1c94c5..24db1241 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,7 @@ jobs: - name: Vitest run: "yarn run test:coverage" - name: Upload to codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5 + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: From e0a8e84df4df1d6a28de902b467cb5aaa15318c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:35:33 +0000 Subject: [PATCH 12/33] Update all non-major dependencies --- package.json | 4 +- yarn.lock | 973 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 677 insertions(+), 300 deletions(-) diff --git a/package.json b/package.json index cc8a36eb..5ed6b755 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "eslint": "^8.14.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.0.0", - "eslint-plugin-deprecate": "^0.8.2", + "eslint-plugin-deprecate": "^0.9.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^61.5.0", "eslint-plugin-jsx-a11y": "^6.5.1", @@ -118,7 +118,7 @@ "qrcode": "^1.5.4", "react": "19", "react-dom": "19", - "react-i18next": "^16.0.0 <16.6.0", + "react-i18next": "^16.0.0 <16.7.0", "react-router-dom": "^7.0.0", "react-use-measure": "^2.1.1", "rxjs": "^7.8.1", diff --git a/yarn.lock b/yarn.lock index cbbbf32f..54f26315 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1466,8 +1466,8 @@ __metadata: linkType: hard "@babel/preset-env@npm:^7.22.20": - version: 7.29.0 - resolution: "@babel/preset-env@npm:7.29.0" + version: 7.29.2 + resolution: "@babel/preset-env@npm:7.29.2" dependencies: "@babel/compat-data": "npm:^7.29.0" "@babel/helper-compilation-targets": "npm:^7.28.6" @@ -1541,7 +1541,7 @@ __metadata: semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/08737e333a538703ba20e9e93b5bfbc01abbb9d3b2519b5b62ad05d3b6b92d79445b1dac91229b8cfcfb0b681b22b7c6fa88d7c1cc15df1690a23b21287f55b6 + checksum: 10c0/d49cb005f2dbc3f2293ab6d80ee8f1380e6215af5518fe26b087c8961c1ea8ebaa554dfce589abe1fbebac25ad7c2515d943dec3859ea2d4981a3f8f4711c580 languageName: node linkType: hard @@ -1623,10 +1623,10 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/runtime@npm:7.28.4" - checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7 +"@babel/runtime@npm:^7.29.2": + version: 7.29.2 + resolution: "@babel/runtime@npm:7.29.2" + checksum: 10c0/30b80a0140d16467792e1bbeb06f655b0dab70407da38dfac7fedae9c859f9ae9d846ef14ad77bd3814c064295fe9b1bc551f1541ea14646ae9f22b71a8bc17a languageName: node linkType: hard @@ -3031,6 +3031,13 @@ __metadata: languageName: node linkType: hard +"@formatjs/bigdecimal@npm:0.2.0": + version: 0.2.0 + resolution: "@formatjs/bigdecimal@npm:0.2.0" + checksum: 10c0/dec607e3d9d4b8c5d0474862e867726cbf322a24d543d5b2cbc3cab6fea187ac787a8e1a0e3df5ceef85a1ab9d58112a08bb7af40b1b3a3b00670431b0603510 + languageName: node + linkType: hard + "@formatjs/ecma402-abstract@npm:2.3.6": version: 2.3.6 resolution: "@formatjs/ecma402-abstract@npm:2.3.6" @@ -3043,15 +3050,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:3.1.1": - version: 3.1.1 - resolution: "@formatjs/ecma402-abstract@npm:3.1.1" +"@formatjs/ecma402-abstract@npm:3.2.0": + version: 3.2.0 + resolution: "@formatjs/ecma402-abstract@npm:3.2.0" dependencies: - "@formatjs/fast-memoize": "npm:3.1.0" - "@formatjs/intl-localematcher": "npm:0.8.1" - decimal.js: "npm:^10.6.0" - tslib: "npm:^2.8.1" - checksum: 10c0/0b4aad9d3917e385d5b090dd1bf6c0a4600851d87149b6a2b552b4f7d31cdf348fcd19ec534cc79efb375997747ae17f9d09633121f4282fac3c5b1cce90ae98 + "@formatjs/bigdecimal": "npm:0.2.0" + "@formatjs/fast-memoize": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.2" + checksum: 10c0/b3c8ac881c3d7533fb4127ca3d771d2a32cb89e6efbbcc72d80b1dcc6a798494ace9ca5ee822b25eb08ebdc7ee2885a9e33496a436b40271ffc915ece605a3ce languageName: node linkType: hard @@ -3064,23 +3070,20 @@ __metadata: languageName: node linkType: hard -"@formatjs/fast-memoize@npm:3.1.0": - version: 3.1.0 - resolution: "@formatjs/fast-memoize@npm:3.1.0" - dependencies: - tslib: "npm:^2.8.1" - checksum: 10c0/367cf8b2816117a3870224a56a3127f2fa5fb854f696102e1cb6229c2f6dec35ccb433fa5343cda76ee5a0a21bff977fad1e4a15f9fba06bcb11f5d4e76d8919 +"@formatjs/fast-memoize@npm:3.1.1": + version: 3.1.1 + resolution: "@formatjs/fast-memoize@npm:3.1.1" + checksum: 10c0/79b24dc1389a49b2b2fb9e90a2ba922a4057d4b74e7bc33a3811f0dc94a5a868d28e8e37917b68c2f831070d11dfd0889de686f269bf5214085a44efc1c25a8c languageName: node linkType: hard "@formatjs/intl-durationformat@npm:^0.10.0": - version: 0.10.1 - resolution: "@formatjs/intl-durationformat@npm:0.10.1" + version: 0.10.3 + resolution: "@formatjs/intl-durationformat@npm:0.10.3" dependencies: - "@formatjs/ecma402-abstract": "npm:3.1.1" - "@formatjs/intl-localematcher": "npm:0.8.1" - tslib: "npm:^2.8.1" - checksum: 10c0/9b0863ba7dd1abc64bbbd15afd24b3bc5ccabe39ab13db97bdfe4d78de7861bd9b0b4a060d6087f1817184328f97b64bfb69ca119071d20dab8f3b5ad8cf9231 + "@formatjs/ecma402-abstract": "npm:3.2.0" + "@formatjs/intl-localematcher": "npm:0.8.2" + checksum: 10c0/dced7b22ad11b83955a3cf60bfcad8b1853ea522cefa70f20c3e1c3607df839f7e388c1f0294fc15375b777ddd0a1d9828e9a19691c4593bb331a6a30d28882e languageName: node linkType: hard @@ -3093,13 +3096,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.8.1": - version: 0.8.1 - resolution: "@formatjs/intl-localematcher@npm:0.8.1" +"@formatjs/intl-localematcher@npm:0.8.2": + version: 0.8.2 + resolution: "@formatjs/intl-localematcher@npm:0.8.2" dependencies: - "@formatjs/fast-memoize": "npm:3.1.0" - tslib: "npm:^2.8.1" - checksum: 10c0/c1ecd407891dec31bc5e9cab7ac4294bfb8c9eb11a5e624d9ae81627fb4dbb27ce38b0efafcfd8b26981b3ea43d765de34238a50474d07fd9556d1e79cfbcc6b + "@formatjs/fast-memoize": "npm:3.1.1" + checksum: 10c0/3bf838a018184837b167964849dafdcdeac95531a24f4df7d868638d4ad716854a250e9bccac9ab4568264c0db7470e70b99363da1db308fdc882b87f3eca651 languageName: node linkType: hard @@ -3323,11 +3325,11 @@ __metadata: linkType: hard "@livekit/protocol@npm:^1.42.2": - version: 1.44.0 - resolution: "@livekit/protocol@npm:1.44.0" + version: 1.45.1 + resolution: "@livekit/protocol@npm:1.45.1" dependencies: "@bufbuild/protobuf": "npm:^1.10.0" - checksum: 10c0/f547a5ee586cae002ed2834f0a823573e38887562dbc793e261791b0572472c6732262a5466c96082464575a3248a4c6cb0428420418e834cdbef1b202cddedf + checksum: 10c0/f249d8501c0021475c4a0a063af5e5d55469fb889b7df4a14a86d347893133bdc8639a8270d80863fe3b8090a3cebf02e7fdc8e4a55aa3985e41ba8e50a2d078 languageName: node linkType: hard @@ -3351,9 +3353,9 @@ __metadata: linkType: hard "@mediapipe/tasks-vision@npm:^0.10.18": - version: 0.10.32 - resolution: "@mediapipe/tasks-vision@npm:0.10.32" - checksum: 10c0/734d472ece8f10e8ba6bdcda7adfc46ddd4da737797e3699aabdb857a4ec5ae87de064b0d7ed41bd96fe49bb8a9420afd1fcc337eea38cd536e7b41bed9f88b7 + version: 0.10.34 + resolution: "@mediapipe/tasks-vision@npm:0.10.34" + checksum: 10c0/7acc1028be2384d0e90dc96e303f01fc5c072d9a25d39a8d157b8576de15ec6e1bf28e31a5f38279fba5aec99197b4be75c058c219d83b5e7aaf85ee0413f124 languageName: node linkType: hard @@ -3537,6 +3539,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:=0.122.0": + version: 0.122.0 + resolution: "@oxc-project/types@npm:0.122.0" + checksum: 10c0/2c64dd0db949426fd0c86d4f61eded5902e7b7b166356a825bd3a248aeaa29a495f78918f66ab78e99644b67bd7556096e2a8123cec74ca4141c604f424f4f74 + languageName: node + linkType: hard + "@oxc-resolver/binding-android-arm-eabi@npm:11.19.1": version: 11.19.1 resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.19.1" @@ -5022,6 +5031,113 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-android-arm64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.12" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.12" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-x64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.12" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.12" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.12" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.12" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.12" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.12" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.12" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.12" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.12" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.12" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.12" + dependencies: + "@napi-rs/wasm-runtime": "npm:^1.1.1" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.12" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.12" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-beta.27": version: 1.0.0-beta.27 resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27" @@ -5029,6 +5145,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.12" + checksum: 10c0/f785d1180ea4876bf6a6a67135822808d1c07f902409524ff1088779f7d5318f6e603d281fb107a5145c1ca54b7cabebd359629ec474ebbc2812f2cf53db4023 + languageName: node + linkType: hard + "@rollup/plugin-inject@npm:^5.0.3": version: 5.0.5 resolution: "@rollup/plugin-inject@npm:5.0.5" @@ -5253,41 +5376,41 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:8.55.0": - version: 8.55.0 - resolution: "@sentry-internal/browser-utils@npm:8.55.0" +"@sentry-internal/browser-utils@npm:8.55.1": + version: 8.55.1 + resolution: "@sentry-internal/browser-utils@npm:8.55.1" dependencies: - "@sentry/core": "npm:8.55.0" - checksum: 10c0/201eb94ee64a4dab058153c64dd4ce0af082f3c3bc84a5441cdadf344d9554a0a67c9d9dfdff720eb42de214d67d734d5bda25a050c2efd59c03f60562bb139a + "@sentry/core": "npm:8.55.1" + checksum: 10c0/161991035e0ffd728f9bbeb7da547cc0cea94e327400beb1b9ed6071ee53387f85adbeefe34d8415a1b8872c1b0e212b955f7951aac18731970fd68c34279597 languageName: node linkType: hard -"@sentry-internal/feedback@npm:8.55.0": - version: 8.55.0 - resolution: "@sentry-internal/feedback@npm:8.55.0" +"@sentry-internal/feedback@npm:8.55.1": + version: 8.55.1 + resolution: "@sentry-internal/feedback@npm:8.55.1" dependencies: - "@sentry/core": "npm:8.55.0" - checksum: 10c0/2515c4eca6226e3df28a498f7f3771d7820556887bf8c06f2d5469c92474cf72ed81eaa0079f6bcf46905c54315e2631bb7b9ed7ed6741cf9b7f73a3f4875acc + "@sentry/core": "npm:8.55.1" + checksum: 10c0/d96c7bdebfe65d079d9eb623d89645e045f3836d7d3a5819b8017939b11f1717a008a97a2bded4869ed7d24d260a18c7d6aeea937e4775c8ad3956e82e532f64 languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:8.55.0": - version: 8.55.0 - resolution: "@sentry-internal/replay-canvas@npm:8.55.0" +"@sentry-internal/replay-canvas@npm:8.55.1": + version: 8.55.1 + resolution: "@sentry-internal/replay-canvas@npm:8.55.1" dependencies: - "@sentry-internal/replay": "npm:8.55.0" - "@sentry/core": "npm:8.55.0" - checksum: 10c0/6f3c619ede1de47635035f74477dd5a11e5c2cac9d0906448a7fffb6dad1c5bd9a49a594fbc2a51ba3b1859a91f60e08ab6de2d9961ccbaa343af580f1d13fb1 + "@sentry-internal/replay": "npm:8.55.1" + "@sentry/core": "npm:8.55.1" + checksum: 10c0/f385c9501c5c2e6a7683e15d26f42b3e08cb5ab7dcdf94a7b4fd58ef9a6d849d3aaaf7b32b15cfbf66d5477b6b79ed8b257784cc7ebc21b99c1d27e47dae55bd languageName: node linkType: hard -"@sentry-internal/replay@npm:8.55.0": - version: 8.55.0 - resolution: "@sentry-internal/replay@npm:8.55.0" +"@sentry-internal/replay@npm:8.55.1": + version: 8.55.1 + resolution: "@sentry-internal/replay@npm:8.55.1" dependencies: - "@sentry-internal/browser-utils": "npm:8.55.0" - "@sentry/core": "npm:8.55.0" - checksum: 10c0/320fd5685c1e84c5feebaa88fc72afd0bd5189b95d690f8c24301cd8b13789431b2c1d28e3e5a93f669ca3b80cdc830e672723aa7a28ff8f0b901674ce0c0529 + "@sentry-internal/browser-utils": "npm:8.55.1" + "@sentry/core": "npm:8.55.1" + checksum: 10c0/65f3ba885a4349530ca42cd3d57fc0dca61c2333b633786a5a818d0b5e59c4dd950aedfd197fa0e9a7f8c7f0ff2ad6caf5a3c030b6cc9671f32c1dd8c4cc4275 languageName: node linkType: hard @@ -5298,16 +5421,16 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:8.55.0": - version: 8.55.0 - resolution: "@sentry/browser@npm:8.55.0" +"@sentry/browser@npm:8.55.1": + version: 8.55.1 + resolution: "@sentry/browser@npm:8.55.1" dependencies: - "@sentry-internal/browser-utils": "npm:8.55.0" - "@sentry-internal/feedback": "npm:8.55.0" - "@sentry-internal/replay": "npm:8.55.0" - "@sentry-internal/replay-canvas": "npm:8.55.0" - "@sentry/core": "npm:8.55.0" - checksum: 10c0/a485de7385851c96ed4c2291d065594aeea2076b11b3b113f4866fdbff1522524abd97664f0d0b011e0eff6c4986a556f080bccfa1b770466c6afcb6122dfbaf + "@sentry-internal/browser-utils": "npm:8.55.1" + "@sentry-internal/feedback": "npm:8.55.1" + "@sentry-internal/replay": "npm:8.55.1" + "@sentry-internal/replay-canvas": "npm:8.55.1" + "@sentry/core": "npm:8.55.1" + checksum: 10c0/5b25013f7a5b57aab4c9d4d3a36aaa0076c0cabf3d2d5f2f01bbef9363aaff5b02e75c7ea48043f3204b97967707a553b78cdb0846a8131cb6c95dde3dfa9843 languageName: node linkType: hard @@ -5423,23 +5546,23 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:8.55.0": - version: 8.55.0 - resolution: "@sentry/core@npm:8.55.0" - checksum: 10c0/51c1768f0bd940a060787b402dba9df3347c918ea4c0fdc300d45c37703ebbf6f7adee9fff332cfd6b23372b33c46e6d2f31a04227762d490aaddc14773894a0 +"@sentry/core@npm:8.55.1": + version: 8.55.1 + resolution: "@sentry/core@npm:8.55.1" + checksum: 10c0/4672f1bb4d3f0fe5aa816d38ee28cd42a19fc66f2cdadbb9b2170e8bb10c7edf3521237cacc81a8ed155c6c68f30ba6922e380401276bf11a6baa1b081542602 languageName: node linkType: hard "@sentry/react@npm:^8.0.0": - version: 8.55.0 - resolution: "@sentry/react@npm:8.55.0" + version: 8.55.1 + resolution: "@sentry/react@npm:8.55.1" dependencies: - "@sentry/browser": "npm:8.55.0" - "@sentry/core": "npm:8.55.0" + "@sentry/browser": "npm:8.55.1" + "@sentry/core": "npm:8.55.1" hoist-non-react-statics: "npm:^3.3.2" peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - checksum: 10c0/09dafee92cb62d3aea5c4503b6d1ad79e293c0e4ad59a60b7700b9d99b18e8e8d6a47e18ed26278d7aa64adbf64c0797c2d096287eeb122a379f5b23b35f597e + checksum: 10c0/7fe204425117090169c62986e54b9b0da7ea0ef2a9fc1a4e9164d7fe75737208733b95a71f2bd4b5a3b7c4326f3a954b03bd2f69ee7b6b2fac75c751b033c1c8 languageName: node linkType: hard @@ -5460,7 +5583,7 @@ __metadata: languageName: node linkType: hard -"@standard-schema/spec@npm:^1.0.0": +"@standard-schema/spec@npm:^1.1.0": version: 1.1.0 resolution: "@standard-schema/spec@npm:1.1.0" checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 @@ -5832,20 +5955,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 25.3.0 - resolution: "@types/node@npm:25.3.0" + version: 25.5.0 + resolution: "@types/node@npm:25.5.0" dependencies: undici-types: "npm:~7.18.0" - checksum: 10c0/7b2b18c9d68047157367fc2f786d4f166d22dc0ad9f82331ca02fb16f2f391854123dbe604dcb938cda119c87051e4bb71dcb9ece44a579f483a6f96d4bd41de + checksum: 10c0/70c508165b6758c4f88d4f91abca526c3985eee1985503d4c2bd994dbaf588e52ac57e571160f18f117d76e963570ac82bd20e743c18987e82564312b3b62119 languageName: node linkType: hard "@types/node@npm:^24.0.0": - version: 24.10.13 - resolution: "@types/node@npm:24.10.13" + version: 24.12.0 + resolution: "@types/node@npm:24.12.0" dependencies: undici-types: "npm:~7.16.0" - checksum: 10c0/4ff0b9b060b5477c0fec5b11a176f294be588104ab546295db65b17a92ba0a6077b52ad92dd3c0d2154198c7f9d0021e6c1d42b00c9ac7ebfd85632afbcc48a4 + checksum: 10c0/8b31c0af5b5474f13048a4e77c57f22cd4f8fe6e58c4b6fde9456b0c13f46a5bfaf5744ff88fd089581de9f0d6e99c584e022681de7acb26a58d258c654c4843 languageName: node linkType: hard @@ -5942,22 +6065,22 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^8.31.0": - version: 8.56.1 - resolution: "@typescript-eslint/eslint-plugin@npm:8.56.1" + version: 8.58.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.58.0" dependencies: "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.56.1" - "@typescript-eslint/type-utils": "npm:8.56.1" - "@typescript-eslint/utils": "npm:8.56.1" - "@typescript-eslint/visitor-keys": "npm:8.56.1" + "@typescript-eslint/scope-manager": "npm:8.58.0" + "@typescript-eslint/type-utils": "npm:8.58.0" + "@typescript-eslint/utils": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.4.0" + ts-api-utils: "npm:^2.5.0" peerDependencies: - "@typescript-eslint/parser": ^8.56.1 + "@typescript-eslint/parser": ^8.58.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/8a97e777792ee3e25078884ba0a04f6732367779c9487abcdc5a2d65b224515fa6a0cf1fac1aafc52fb30f3af97f2e1c9949aadbd6ca74a0165691f95494a721 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/ac45c30f6ba9e188a01144708aa845e7ee8bb8a4d4f9aa6d2dce7784852d0821d42b031fee6832069935c3b885feff6d4014e30145b99693d25d7f563266a9f8 languageName: node linkType: hard @@ -5973,31 +6096,31 @@ __metadata: linkType: hard "@typescript-eslint/parser@npm:^8.31.0": - version: 8.56.1 - resolution: "@typescript-eslint/parser@npm:8.56.1" + version: 8.58.0 + resolution: "@typescript-eslint/parser@npm:8.58.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.56.1" - "@typescript-eslint/types": "npm:8.56.1" - "@typescript-eslint/typescript-estree": "npm:8.56.1" - "@typescript-eslint/visitor-keys": "npm:8.56.1" + "@typescript-eslint/scope-manager": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/typescript-estree": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" debug: "npm:^4.4.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/61c9dab481e795b01835c00c9c7c845f1d7ea7faf3b8657fccee0f8658a65390cb5fe2b5230ae8c4241bd6e0c32aa9455a91989a492bd3bd6fec7c7d9339377a + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/56c7ec21675cec4730760bfa37c29e42e80b4d6444e2beca55fad9ef53731392270d142797482ea798405be0d7e28ec6c9c16a1ee2ee1c94f73d3bf0ed29763c languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.56.1": - version: 8.56.1 - resolution: "@typescript-eslint/project-service@npm:8.56.1" +"@typescript-eslint/project-service@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/project-service@npm:8.58.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.56.1" - "@typescript-eslint/types": "npm:^8.56.1" + "@typescript-eslint/tsconfig-utils": "npm:^8.58.0" + "@typescript-eslint/types": "npm:^8.58.0" debug: "npm:^4.4.3" peerDependencies: - typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/ca61cde575233bc79046d73ddd330d183fb3cbb941fddc31919336317cda39885c59296e2e5401b03d9325a64a629e842fd66865705ff0d85d83ee3ee40871e8 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/e6d0cb2f7708ccb31a2ff9eb35817d4999c26e1f1cd3c607539e21d0c73a234daa77c73ee1163bc4e8b139252d619823c444759f1ddabdd138cab4885e9c9794 languageName: node linkType: hard @@ -6021,38 +6144,38 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.56.1": - version: 8.56.1 - resolution: "@typescript-eslint/scope-manager@npm:8.56.1" +"@typescript-eslint/scope-manager@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/scope-manager@npm:8.58.0" dependencies: - "@typescript-eslint/types": "npm:8.56.1" - "@typescript-eslint/visitor-keys": "npm:8.56.1" - checksum: 10c0/89cc1af2635eee23f2aa2ff87c08f88f3ad972ebf67eaacdc604a4ef4178535682bad73fd086e6f3c542e4e5d874253349af10d58291d079cc29c6c7e9831de4 + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" + checksum: 10c0/bd5c16780f22d62359af0f69909f38a15fa3c55e609124a7cd5c2a04322fe41e586d81066f3ad1dcc3c1eff24dbcb48b78d099626d611fbd680c20c005d48f1d languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.56.1, @typescript-eslint/tsconfig-utils@npm:^8.56.1": - version: 8.56.1 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.56.1" +"@typescript-eslint/tsconfig-utils@npm:8.58.0, @typescript-eslint/tsconfig-utils@npm:^8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.0" peerDependencies: - typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/d03b64d7ff19020beeefa493ae667c2e67a4547d25a3ecb9210a3a52afe980c093d772a91014bae699ee148bfb60cc659479e02bfc2946ea06954a8478ef1fe1 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/0a07fe1a28b2513e625882bc8d4c4e0c5a105cdbcb987beae12fc66dbe71dc9638013e4d1fa8ad10d828a2acd5e3fed987c189c00d41fed0e880009f99adf1b2 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.56.1": - version: 8.56.1 - resolution: "@typescript-eslint/type-utils@npm:8.56.1" +"@typescript-eslint/type-utils@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/type-utils@npm:8.58.0" dependencies: - "@typescript-eslint/types": "npm:8.56.1" - "@typescript-eslint/typescript-estree": "npm:8.56.1" - "@typescript-eslint/utils": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/typescript-estree": "npm:8.58.0" + "@typescript-eslint/utils": "npm:8.58.0" debug: "npm:^4.4.3" - ts-api-utils: "npm:^2.4.0" + ts-api-utils: "npm:^2.5.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/66517aed5059ef4a29605d06a510582f934d5789ae40ad673f1f0421f8aa13ec9ba7b8caab57ae9f270afacbf13ec5359cedfe74f21ae77e9a2364929f7e7cee + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/1223733d41f8463be92ef1ad048d546f9663152212b22dc968abbd9f8e4486bd4082e16baa51d2d281e0d4815563bc4b1ecf01684e2940b7897ba17aa26d1196 languageName: node linkType: hard @@ -6070,7 +6193,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.56.1, @typescript-eslint/types@npm:^8.46.4, @typescript-eslint/types@npm:^8.56.1": +"@typescript-eslint/types@npm:8.58.0, @typescript-eslint/types@npm:^8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/types@npm:8.58.0" + checksum: 10c0/f2fe1321758a04591c20d77caba956ae76b77cff0b976a0224b37077d80b1ebd826874d15ec79c3a3b7d57ee5679e5d10756db1b082bde3d51addbd3a8431d38 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:^8.46.4": version: 8.56.1 resolution: "@typescript-eslint/types@npm:8.56.1" checksum: 10c0/e5a0318abddf0c4f98da3039cb10b3c0601c8601f7a9f7043630f0d622dabfe83a4cd833545ad3531fc846e46ca2874377277b392c2490dffec279d9242d827b @@ -6113,22 +6243,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.56.1": - version: 8.56.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.56.1" +"@typescript-eslint/typescript-estree@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.58.0" dependencies: - "@typescript-eslint/project-service": "npm:8.56.1" - "@typescript-eslint/tsconfig-utils": "npm:8.56.1" - "@typescript-eslint/types": "npm:8.56.1" - "@typescript-eslint/visitor-keys": "npm:8.56.1" + "@typescript-eslint/project-service": "npm:8.58.0" + "@typescript-eslint/tsconfig-utils": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" debug: "npm:^4.4.3" minimatch: "npm:^10.2.2" semver: "npm:^7.7.3" tinyglobby: "npm:^0.2.15" - ts-api-utils: "npm:^2.4.0" + ts-api-utils: "npm:^2.5.0" peerDependencies: - typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/92f4421dac41be289761200dc2ed85974fa451deacb09490ae1870a25b71b97218e609a90d4addba9ded5b2abdebc265c9db7f6e9ce6d29ed20e89b8487e9618 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/a8cb94cb765b27740a54f9b5378bd8f0dc49e301ceed99a0791dc9d1f61c2a54e3212f7ed9120c8c2df80104ad3117150cf5e7fe8a0b7eec3ed04969a79b103e languageName: node linkType: hard @@ -6150,18 +6280,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.56.1": - version: 8.56.1 - resolution: "@typescript-eslint/utils@npm:8.56.1" +"@typescript-eslint/utils@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/utils@npm:8.58.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.56.1" - "@typescript-eslint/types": "npm:8.56.1" - "@typescript-eslint/typescript-estree": "npm:8.56.1" + "@typescript-eslint/scope-manager": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/typescript-estree": "npm:8.58.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/d9ffd9b2944a2c425e0532f71dc61e61d0a923d1a17733cf2777c2a4ae638307d12d44f63b33b6b3dc62f02f47db93ec49344ecefe17b76ee3e4fb0833325be3 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/457e01a6e6d954dbfe13c49ece3cf8a55e5d8cf19ea9ae7086c0e205d89e3cdbb91153062ab440d2e78ad3f077b174adc42bfb1b6fc24299020a0733e7f9c11c languageName: node linkType: hard @@ -6200,13 +6330,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.56.1": - version: 8.56.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.56.1" +"@typescript-eslint/visitor-keys@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.58.0" dependencies: - "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.58.0" eslint-visitor-keys: "npm:^5.0.0" - checksum: 10c0/86d97905dec1af964cc177c185933d040449acf6006096497f2e0093c6a53eb92b3ac1db9eb40a5a2e8d91160f558c9734331a9280797f09f284c38978b22190 + checksum: 10c0/75f3c9c097a308cc6450822a0f81d44c8b79b524e99dd2c41ded347b12f148ab3bd459ce9cc6bd00f8f0725c5831baab6d2561596ead3394ab76dddbeb32cce1 languageName: node linkType: hard @@ -6293,68 +6423,68 @@ __metadata: linkType: hard "@vitest/coverage-v8@npm:^4.0.18": - version: 4.0.18 - resolution: "@vitest/coverage-v8@npm:4.0.18" + version: 4.1.2 + resolution: "@vitest/coverage-v8@npm:4.1.2" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.0.18" - ast-v8-to-istanbul: "npm:^0.3.10" + "@vitest/utils": "npm:4.1.2" + ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" istanbul-reports: "npm:^3.2.0" - magicast: "npm:^0.5.1" + magicast: "npm:^0.5.2" obug: "npm:^2.1.1" - std-env: "npm:^3.10.0" - tinyrainbow: "npm:^3.0.3" + std-env: "npm:^4.0.0-rc.1" + tinyrainbow: "npm:^3.1.0" peerDependencies: - "@vitest/browser": 4.0.18 - vitest: 4.0.18 + "@vitest/browser": 4.1.2 + vitest: 4.1.2 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10c0/e23e0da86f0b2a020c51562bc40ebdc7fc7553c24f8071dfb39a6df0161badbd5eaf2eebbf8ceaef18933a18c1934ff52d1c0c4bde77bb87e0c1feb0c8cbee4d + checksum: 10c0/2f4488efb34a5d9e3a70631ba263e153eecba8ec0da52cb874cdc674c88369061706572b9fc0c302376fd1de58aedc86a2f9f75e5a521084e31a1446c85f2c40 languageName: node linkType: hard -"@vitest/expect@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/expect@npm:4.0.18" +"@vitest/expect@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/expect@npm:4.1.2" dependencies: - "@standard-schema/spec": "npm:^1.0.0" + "@standard-schema/spec": "npm:^1.1.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.0.18" - "@vitest/utils": "npm:4.0.18" - chai: "npm:^6.2.1" - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/123b0aa111682e82ec5289186df18037b1a1768700e468ee0f9879709aaa320cf790463c15c0d8ee10df92b402f4394baf5d27797e604d78e674766d87bcaadc + "@vitest/spy": "npm:4.1.2" + "@vitest/utils": "npm:4.1.2" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/e238c833b5555d31b074545807956d5e874a1ef725525ecc99f1885b71b230b2127d40d8d142a7253666b8565d5806723853e85e0e99265520ec7506fdc5890c languageName: node linkType: hard -"@vitest/mocker@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/mocker@npm:4.0.18" +"@vitest/mocker@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/mocker@npm:4.1.2" dependencies: - "@vitest/spy": "npm:4.0.18" + "@vitest/spy": "npm:4.1.2" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - checksum: 10c0/fb0a257e7e167759d4ad228d53fa7bad2267586459c4a62188f2043dd7163b4b02e1e496dc3c227837f776e7d73d6c4343613e89e7da379d9d30de8260f1ee4b + checksum: 10c0/f23094f3c7e1e5af42e6a468f0815c1ecdcab85cb3a56ab6f3f214a9808a40271467d4352cae972482b9738cc31c62c7312d8b0da227d6ea03d2b3aacb8d385f languageName: node linkType: hard -"@vitest/pretty-format@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/pretty-format@npm:4.0.18" +"@vitest/pretty-format@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/pretty-format@npm:4.1.2" dependencies: - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/0086b8c88eeca896d8e4b98fcdef452c8041a1b63eb9e85d3e0bcc96c8aa76d8e9e0b6990ebb0bb0a697c4ebab347e7735888b24f507dbff2742ddce7723fd94 + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/6f57519c707e6a3d1ff8630ca87ce78fda9bf7bb33f6e4a0c775a8b510f2a6cee109849e2cdb736b0280681c567bd03e4cff724cbf0962950c9ff81377f0b2bc languageName: node linkType: hard @@ -6367,41 +6497,43 @@ __metadata: languageName: node linkType: hard -"@vitest/runner@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/runner@npm:4.0.18" +"@vitest/runner@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/runner@npm:4.1.2" dependencies: - "@vitest/utils": "npm:4.0.18" + "@vitest/utils": "npm:4.1.2" pathe: "npm:^2.0.3" - checksum: 10c0/fdb4afa411475133c05ba266c8092eaf1e56cbd5fb601f92ec6ccb9bab7ca52e06733ee8626599355cba4ee71cb3a8f28c84d3b69dc972e41047edc50229bc01 + checksum: 10c0/35654a87bd27983443adc24d68529d624f7d70e0386176741dc5bcc4188b86a70af2c512405d7e97aa45c16d83e1c8566c1f99c8440430f95557275f18612d21 languageName: node linkType: hard -"@vitest/snapshot@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/snapshot@npm:4.0.18" +"@vitest/snapshot@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/snapshot@npm:4.1.2" dependencies: - "@vitest/pretty-format": "npm:4.0.18" + "@vitest/pretty-format": "npm:4.1.2" + "@vitest/utils": "npm:4.1.2" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10c0/d3bfefa558db9a69a66886ace6575eb96903a5ba59f4d9a5d0fecb4acc2bb8dbb443ef409f5ac1475f2e1add30bd1d71280f98912da35e89c75829df9e84ea43 + checksum: 10c0/6d20e92386937afddbc81344211e554b83a559e20fb10c1deb0b1c3532994dc9fc62d816706ac835bdb737eb1ab02e9c0bc9de80dd8316060e1e0aaa447ba48f languageName: node linkType: hard -"@vitest/spy@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/spy@npm:4.0.18" - checksum: 10c0/6de537890b3994fcadb8e8d8ac05942320ae184f071ec395d978a5fba7fa928cbb0c5de85af86a1c165706c466e840de8779eaff8c93450c511c7abaeb9b8a4e +"@vitest/spy@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/spy@npm:4.1.2" + checksum: 10c0/2b5888d536d3e2083c5f8939763e6d780c2c03cc60e1ab45f9d04eacf14467acb9724cae1c4778e4c06426d49d04517e190122882953054a4b13fda44780bb14 languageName: node linkType: hard -"@vitest/utils@npm:4.0.18": - version: 4.0.18 - resolution: "@vitest/utils@npm:4.0.18" +"@vitest/utils@npm:4.1.2": + version: 4.1.2 + resolution: "@vitest/utils@npm:4.1.2" dependencies: - "@vitest/pretty-format": "npm:4.0.18" - tinyrainbow: "npm:^3.0.3" - checksum: 10c0/4a3c43c1421eb90f38576926496f6c80056167ba111e63f77cf118983902673737a1a38880b890d7c06ec0a12475024587344ee502b3c43093781533022f2aeb + "@vitest/pretty-format": "npm:4.1.2" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/d96475e0703b6e5208c6c0f570c1235278cbac3f3913a9aa4203a3e617c9eaca85a184bfd5d13cf366b84754df787ab8bc85242c5e0c63105ee7176c186a2136 languageName: node linkType: hard @@ -6749,14 +6881,14 @@ __metadata: languageName: node linkType: hard -"ast-v8-to-istanbul@npm:^0.3.10": - version: 0.3.12 - resolution: "ast-v8-to-istanbul@npm:0.3.12" +"ast-v8-to-istanbul@npm:^1.0.0": + version: 1.0.0 + resolution: "ast-v8-to-istanbul@npm:1.0.0" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.31" estree-walker: "npm:^3.0.3" js-tokens: "npm:^10.0.0" - checksum: 10c0/bad6ba222b1073c165c8d65dbf366193d4a90536dabe37f93a3df162269b1c9473975756e4c048f708c235efccc26f8e5321c547b7e9563b64b21b2e0f27cbc9 + checksum: 10c0/35e57b754ba63287358094d4f7ae8de2de27286fb4e76a1fbf28b2e67e3b670b59c3f511882473d0fd2cdbaa260062e3cd4f216b724c70032e2b09e5cebbd618 languageName: node linkType: hard @@ -7319,7 +7451,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:^6.2.1": +"chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 @@ -8002,13 +8134,6 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.6.0": - version: 10.6.0 - resolution: "decimal.js@npm:10.6.0" - checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa - languageName: node - linkType: hard - "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -8071,6 +8196,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.3": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + "detect-node-es@npm:^1.1.0": version: 1.1.0 resolution: "detect-node-es@npm:1.1.0" @@ -8334,7 +8466,7 @@ __metadata: eslint: "npm:^8.14.0" eslint-config-google: "npm:^0.14.0" eslint-config-prettier: "npm:^10.0.0" - eslint-plugin-deprecate: "npm:^0.8.2" + eslint-plugin-deprecate: "npm:^0.9.0" eslint-plugin-import: "npm:^2.26.0" eslint-plugin-jsdoc: "npm:^61.5.0" eslint-plugin-jsx-a11y: "npm:^6.5.1" @@ -8366,7 +8498,7 @@ __metadata: qrcode: "npm:^1.5.4" react: "npm:19" react-dom: "npm:19" - react-i18next: "npm:^16.0.0 <16.6.0" + react-i18next: "npm:^16.0.0 <16.7.0" react-router-dom: "npm:^7.0.0" react-use-measure: "npm:^2.1.1" rxjs: "npm:^7.8.1" @@ -8644,10 +8776,10 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^1.7.0": - version: 1.7.0 - resolution: "es-module-lexer@npm:1.7.0" - checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b +"es-module-lexer@npm:^2.0.0": + version: 2.0.0 + resolution: "es-module-lexer@npm:2.0.0" + checksum: 10c0/ae78dbbd43035a4b972c46cfb6877e374ea290adfc62bc2f5a083fea242c0b2baaab25c5886af86be55f092f4a326741cb94334cd3c478c383fdc8a9ec5ff817 languageName: node linkType: hard @@ -8963,12 +9095,12 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-deprecate@npm:^0.8.2": - version: 0.8.7 - resolution: "eslint-plugin-deprecate@npm:0.8.7" +"eslint-plugin-deprecate@npm:^0.9.0": + version: 0.9.0 + resolution: "eslint-plugin-deprecate@npm:0.9.0" peerDependencies: - eslint: ">=2.x" - checksum: 10c0/7f40cd7dab8ae62b1ce75f8c71de3858c1e10cb1a8fbfddc4c16474f2ca7b125d33897fd78b272c61c693849761da6a7fef039d34454dcaeba757b774f14bb23 + eslint: ^8.0.0 || ^9.0.0 + checksum: 10c0/a1be5d100b48220bbe8692fca3671db7218cb86b636f7403542f524e1a075bc1ed2c42b15b2fd0bb096b07180c08e5d8e5467ef6e19676edadde36b0c35785d0 languageName: node linkType: hard @@ -9363,7 +9495,7 @@ __metadata: languageName: node linkType: hard -"expect-type@npm:^1.2.2": +"expect-type@npm:^1.3.0": version: 1.3.0 resolution: "expect-type@npm:1.3.0" checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd @@ -10233,8 +10365,8 @@ __metadata: linkType: hard "i18next-parser@npm:^9.1.0": - version: 9.3.0 - resolution: "i18next-parser@npm:9.3.0" + version: 9.4.0 + resolution: "i18next-parser@npm:9.4.0" dependencies: "@babel/runtime": "npm:^7.25.0" broccoli-plugin: "npm:^4.0.7" @@ -10255,7 +10387,7 @@ __metadata: vinyl-fs: "npm:^4.0.0" bin: i18next: bin/cli.js - checksum: 10c0/dd9de4d6812da662eaefafcaf6dae9c88d7e98c9907f784257056408bb22ac5ae23659bbfdf975452bfc35595914e280de0ef7c9f313cbd1e4cdb12dd0dadc1e + checksum: 10c0/4a9c94d16ea0869e58cd600216d56e6ccb2d815bf5acdb96df1aba0750d053514abd52136434681de3601bc0f5324464b8a42f608bfe6c95961f5c2a2e38e3da languageName: node linkType: hard @@ -10274,16 +10406,16 @@ __metadata: linkType: hard "i18next@npm:^25.0.0": - version: 25.8.13 - resolution: "i18next@npm:25.8.13" + version: 25.10.10 + resolution: "i18next@npm:25.10.10" dependencies: - "@babel/runtime": "npm:^7.28.4" + "@babel/runtime": "npm:^7.29.2" peerDependencies: - typescript: ^5 + typescript: ^5 || ^6 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/12c661c2b58fe70445f8491b72f937eef28a5f9413f76bd178bbca92d4378d8436003c3bea1d5d760b8a69f809cbcef2ce389beffd9bc0434651134c6b37fecc + checksum: 10c0/6ff601b52363c9c974ef293af4a6de04d9f76b4bf2ad9a56c8546235aaf19ff1a7c2109ddba3a73922d418e208ec5b688e5afb323eba6e27c75d629e93827ae1 languageName: node linkType: hard @@ -10317,7 +10449,7 @@ __metadata: languageName: node linkType: hard -"immutable@npm:^5.0.2": +"immutable@npm:^5.1.5": version: 5.1.5 resolution: "immutable@npm:5.1.5" checksum: 10c0/8017ece1578e3c5939ba3305176aee059def1b8a90c7fa2a347ef583ebbd38cbe77ce1bbd786a5fab57e2da00bbcb0493b92e4332cdc4e1fe5cfb09a4688df31 @@ -11082,8 +11214,8 @@ __metadata: linkType: hard "knip@npm:^5.86.0": - version: 5.86.0 - resolution: "knip@npm:5.86.0" + version: 5.88.1 + resolution: "knip@npm:5.88.1" dependencies: "@nodelib/fs.walk": "npm:^1.2.3" fast-glob: "npm:^3.3.3" @@ -11104,7 +11236,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10c0/6905c3c2bd21b1f5d51bf83568d1eff67d9d74dd9547c428f810b0dbc3624225a0c41b8e8caccbb111df2db175933aa853345798a05f91f9344ce3aca26898ff + checksum: 10c0/79a34e1d8e5bf5ead2b98a83159ce897e87c0de136a30eebe34ac61c6c465a61d0f6d1177cd76f69bda7939248d5c56611f8c52e8bcb5400d93964b908968ced languageName: node linkType: hard @@ -11141,6 +11273,126 @@ __metadata: languageName: node linkType: hard +"lightningcss-android-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-android-arm64@npm:1.32.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-arm64@npm:1.32.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-x64@npm:1.32.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-freebsd-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-freebsd-x64@npm:1.32.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-linux-arm-gnueabihf@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"lightningcss-linux-arm64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-arm64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-linux-x64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-x64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-musl@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-win32-arm64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-win32-x64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lightningcss@npm:^1.32.0": + version: 1.32.0 + resolution: "lightningcss@npm:1.32.0" + dependencies: + detect-libc: "npm:^2.0.3" + lightningcss-android-arm64: "npm:1.32.0" + lightningcss-darwin-arm64: "npm:1.32.0" + lightningcss-darwin-x64: "npm:1.32.0" + lightningcss-freebsd-x64: "npm:1.32.0" + lightningcss-linux-arm-gnueabihf: "npm:1.32.0" + lightningcss-linux-arm64-gnu: "npm:1.32.0" + lightningcss-linux-arm64-musl: "npm:1.32.0" + lightningcss-linux-x64-gnu: "npm:1.32.0" + lightningcss-linux-x64-musl: "npm:1.32.0" + lightningcss-win32-arm64-msvc: "npm:1.32.0" + lightningcss-win32-x64-msvc: "npm:1.32.0" + dependenciesMeta: + lightningcss-android-arm64: + optional: true + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10c0/70945bd55097af46fc9fab7f5ed09cd5869d85940a2acab7ee06d0117004a1d68155708a2d462531cea2fc3c67aefc9333a7068c80b0b78dd404c16838809e03 + languageName: node + linkType: hard + "lilconfig@npm:^3.1.3": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" @@ -11298,7 +11550,7 @@ __metadata: languageName: node linkType: hard -"magicast@npm:^0.5.1": +"magicast@npm:^0.5.2": version: 0.5.2 resolution: "magicast@npm:0.5.2" dependencies: @@ -12319,6 +12571,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0 + languageName: node + linkType: hard + "pkg-dir@npm:^5.0.0": version: 5.0.0 resolution: "pkg-dir@npm:5.0.0" @@ -12761,14 +13020,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.41, postcss@npm:^8.5.6": - version: 8.5.6 - resolution: "postcss@npm:8.5.6" +"postcss@npm:^8.4.41, postcss@npm:^8.5.6, postcss@npm:^8.5.8": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/5127cc7c91ed7a133a1b7318012d8bfa112da9ef092dddf369ae699a1f10ebbd89b1b9f25f3228795b84585c72aabd5ced5fc11f2ba467eedf7b081a66fad024 + checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c languageName: node linkType: hard @@ -12995,17 +13254,17 @@ __metadata: languageName: node linkType: hard -"react-i18next@npm:^16.0.0 <16.6.0": - version: 16.5.4 - resolution: "react-i18next@npm:16.5.4" +"react-i18next@npm:^16.0.0 <16.7.0": + version: 16.6.6 + resolution: "react-i18next@npm:16.6.6" dependencies: - "@babel/runtime": "npm:^7.28.4" + "@babel/runtime": "npm:^7.29.2" html-parse-stringify: "npm:^3.0.1" use-sync-external-store: "npm:^1.6.0" peerDependencies: - i18next: ">= 25.6.2" + i18next: ">= 25.10.9" react: ">= 16.8.0" - typescript: ^5 + typescript: ^5 || ^6 peerDependenciesMeta: react-dom: optional: true @@ -13013,7 +13272,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10c0/41d0b76873addfa3abe0c6b8a10a796e01f205f3636bc2d090d0078b42222f2949c4303f18d7a80cc26cf1298918cb6220d96e39ae2b8644abfdbec3bb504b37 + checksum: 10c0/7d6660d382a529d4dc56e7a7bbab59689ac0f435b3a18bba81d6bbf52c87b2a41fd5d1bffcf60b92b5e6e2a1ad9443949fcf16329d9c63d582316557199a7d32 languageName: node linkType: hard @@ -13093,20 +13352,20 @@ __metadata: linkType: hard "react-router-dom@npm:^7.0.0": - version: 7.13.1 - resolution: "react-router-dom@npm:7.13.1" + version: 7.13.2 + resolution: "react-router-dom@npm:7.13.2" dependencies: - react-router: "npm:7.13.1" + react-router: "npm:7.13.2" peerDependencies: react: ">=18" react-dom: ">=18" - checksum: 10c0/2b8ed9dc753f1f7be599a53a00900df04e2b4d1186b0a4d63004eebb2250cd78cd6837ff15fcada5f88d53ad127fff0d1de31468715dcd6dd79dad8cfa8414e9 + checksum: 10c0/ce9e5d47ba8accb7aa40272f83a98c3372634d0216ec92c8b20d52765ca62db06b4fde5284a4bd37cef2279b9767a45927f3b074e74e2c9b462c8599427bbfef languageName: node linkType: hard -"react-router@npm:7.13.1": - version: 7.13.1 - resolution: "react-router@npm:7.13.1" +"react-router@npm:7.13.2": + version: 7.13.2 + resolution: "react-router@npm:7.13.2" dependencies: cookie: "npm:^1.0.1" set-cookie-parser: "npm:^2.6.0" @@ -13116,7 +13375,7 @@ __metadata: peerDependenciesMeta: react-dom: optional: true - checksum: 10c0/a64c645cede74251f21483fbfad740b36dc5133522d6f53f12317a873a22865fce659d4c2377d5e19c912f85c7b12b88224a2c70d8f70c082496b569cc4abc31 + checksum: 10c0/c7620565df0b444507cfceb76733adbd26e28a72fc4d52d344190bcb2e6483427313a3b6076c53d362e2682ccdb1262731bcaa6c3577cbbeea130291a38715f1 languageName: node linkType: hard @@ -13584,6 +13843,64 @@ __metadata: languageName: node linkType: hard +"rolldown@npm:1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "rolldown@npm:1.0.0-rc.12" + dependencies: + "@oxc-project/types": "npm:=0.122.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.12" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.12" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.12" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.12" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.12" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.12" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.12" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.12" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.12" + "@rolldown/pluginutils": "npm:1.0.0-rc.12" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: bin/cli.mjs + checksum: 10c0/0c4e5e3cdcdddce282cb2d84e1c98d6ad8d4e452d5c1402e498b35ec1060026e552dd783efc9f4ba876d7c0863b5973edc79b6a546f565e9832dc1077ec18c2c + languageName: node + linkType: hard + "rollup@npm:^4.43.0": version: 4.59.0 resolution: "rollup@npm:4.59.0" @@ -13786,19 +14103,19 @@ __metadata: linkType: hard "sass@npm:^1.42.1": - version: 1.97.3 - resolution: "sass@npm:1.97.3" + version: 1.98.0 + resolution: "sass@npm:1.98.0" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" - immutable: "npm:^5.0.2" + immutable: "npm:^5.1.5" source-map-js: "npm:>=0.6.2 <2.0.0" dependenciesMeta: "@parcel/watcher": optional: true bin: sass: sass.js - checksum: 10c0/67f6b5d220f20c1c23a8b16dda5fd1c5d119ad5caf8195b185d553b5b239fb188a3787f04fc00171c62515f2c4e5e0eb5ad4992a80f8543428556883c1240ba3 + checksum: 10c0/9e91daa20f970fefb364ac31289f070636da7aa7eaeb43e371ea98fa98085a6dbc2d3d058504226a02d07717faf0a4ce8d41b579ecb428c4a9d96b4dc1944a95 languageName: node linkType: hard @@ -14196,10 +14513,10 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.10.0": - version: 3.10.0 - resolution: "std-env@npm:3.10.0" - checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f +"std-env@npm:^4.0.0-rc.1": + version: 4.0.0 + resolution: "std-env@npm:4.0.0" + checksum: 10c0/63b1716eae27947adde49e21b7225a0f75fb2c3d410273ae9de8333c07c7d5fc7a0628ae4c8af6b4b49b4274ed46c2bf118ed69b64f1261c9d8213d76ed1c16c languageName: node linkType: hard @@ -14480,15 +14797,15 @@ __metadata: linkType: hard "tar@npm:^7.5.11": - version: 7.5.11 - resolution: "tar@npm:7.5.11" + version: 7.5.13 + resolution: "tar@npm:7.5.13" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/b6bb420550ef50ef23356018155e956cd83282c97b6128d8d5cfe5740c57582d806a244b2ef0bf686a74ce526babe8b8b9061527623e935e850008d86d838929 + checksum: 10c0/5c65b8084799bde7a791593a1c1a45d3d6ee98182e3700b24c247b7b8f8654df4191642abbdb07ff25043d45dcff35620827c3997b88ae6c12040f64bed5076b languageName: node linkType: hard @@ -14581,7 +14898,7 @@ __metadata: languageName: node linkType: hard -"tinyrainbow@npm:^3.0.3": +"tinyrainbow@npm:^3.1.0": version: 3.1.0 resolution: "tinyrainbow@npm:3.1.0" checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 @@ -14695,12 +15012,12 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.4.0": - version: 2.4.0 - resolution: "ts-api-utils@npm:2.4.0" +"ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" peerDependencies: typescript: ">=4.8.4" - checksum: 10c0/ed185861aef4e7124366a3f6561113557a57504267d4d452a51e0ba516a9b6e713b56b4aeaab9fa13de9db9ab755c65c8c13a777dba9133c214632cb7b65c083 + checksum: 10c0/767849383c114e7f1971fa976b20e73ac28fd0c70d8d65c0004790bf4d8f89888c7e4cf6d5949f9c1beae9bc3c64835bef77bbe27fddf45a3c7b60cebcf85c8c languageName: node linkType: hard @@ -14723,7 +15040,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -15350,7 +15667,64 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.3.0": +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.0.3 + resolution: "vite@npm:8.0.3" + dependencies: + fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.32.0" + picomatch: "npm:^4.0.4" + postcss: "npm:^8.5.8" + rolldown: "npm:1.0.0-rc.12" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.1.0 + esbuild: ^0.27.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/bed9520358080393a02fe22565b3309b4b3b8f916afe4c97577528f3efb05c1bf4b29f7b552179bc5b3938629e50fbd316231727457411dbc96648fa5c9d14bf + languageName: node + linkType: hard + +"vite@npm:^7.3.0": version: 7.3.1 resolution: "vite@npm:7.3.1" dependencies: @@ -15420,39 +15794,40 @@ __metadata: linkType: hard "vitest@npm:^4.0.18": - version: 4.0.18 - resolution: "vitest@npm:4.0.18" + version: 4.1.2 + resolution: "vitest@npm:4.1.2" dependencies: - "@vitest/expect": "npm:4.0.18" - "@vitest/mocker": "npm:4.0.18" - "@vitest/pretty-format": "npm:4.0.18" - "@vitest/runner": "npm:4.0.18" - "@vitest/snapshot": "npm:4.0.18" - "@vitest/spy": "npm:4.0.18" - "@vitest/utils": "npm:4.0.18" - es-module-lexer: "npm:^1.7.0" - expect-type: "npm:^1.2.2" + "@vitest/expect": "npm:4.1.2" + "@vitest/mocker": "npm:4.1.2" + "@vitest/pretty-format": "npm:4.1.2" + "@vitest/runner": "npm:4.1.2" + "@vitest/snapshot": "npm:4.1.2" + "@vitest/spy": "npm:4.1.2" + "@vitest/utils": "npm:4.1.2" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" obug: "npm:^2.1.1" pathe: "npm:^2.0.3" picomatch: "npm:^4.0.3" - std-env: "npm:^3.10.0" + std-env: "npm:^4.0.0-rc.1" tinybench: "npm:^2.9.0" tinyexec: "npm:^1.0.2" tinyglobby: "npm:^0.2.15" - tinyrainbow: "npm:^3.0.3" - vite: "npm:^6.0.0 || ^7.0.0" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.0.18 - "@vitest/browser-preview": 4.0.18 - "@vitest/browser-webdriverio": 4.0.18 - "@vitest/ui": 4.0.18 + "@vitest/browser-playwright": 4.1.2 + "@vitest/browser-preview": 4.1.2 + "@vitest/browser-webdriverio": 4.1.2 + "@vitest/ui": 4.1.2 happy-dom: "*" jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: "@edge-runtime/vm": optional: true @@ -15472,9 +15847,11 @@ __metadata: optional: true jsdom: optional: true + vite: + optional: false bin: vitest: vitest.mjs - checksum: 10c0/b913cd32032c95f29ff08c931f4b4c6fd6d2da498908d6770952c561a1b8d75c62499a1f04cadf82fb89cc0f9a33f29fb5dfdb899f6dbb27686a9d91571be5fa + checksum: 10c0/061fdd0319ba533c926b139b9377a7dbf91e63d815d86fe318a207bd19842b74ca6f6402ea61b26ed9d2924306bdb4d0b13f69c29e2a2a89b3b67602bcccb54c languageName: node linkType: hard From 5e337a53998a8d320826d46ef7786e73c15ae462 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:36:05 +0000 Subject: [PATCH 13/33] Update dependency @vector-im/compound-design-tokens to v8 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cc8a36eb..4c401ea2 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@typescript-eslint/eslint-plugin": "^8.31.0", "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", - "@vector-im/compound-design-tokens": "^6.0.0", + "@vector-im/compound-design-tokens": "^8.0.0", "@vector-im/compound-web": "^8.0.0", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", diff --git a/yarn.lock b/yarn.lock index cbbbf32f..c91c3071 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6235,9 +6235,9 @@ __metadata: languageName: node linkType: hard -"@vector-im/compound-design-tokens@npm:^6.0.0": - version: 6.10.2 - resolution: "@vector-im/compound-design-tokens@npm:6.10.2" +"@vector-im/compound-design-tokens@npm:^8.0.0": + version: 8.0.0 + resolution: "@vector-im/compound-design-tokens@npm:8.0.0" peerDependencies: "@types/react": "*" react: ^17 || ^18 || ^19.0.0 @@ -6246,7 +6246,7 @@ __metadata: optional: true react: optional: true - checksum: 10c0/bcac6d79fcfb8cc1356d65dff576bdad217edd0df189a5dea032b0fd57cef335b73ad6d8e395709245bc1c6a8c672a83144ecea48550ca560544d2399af8f2d3 + checksum: 10c0/caa7a5ba9930b9f2eb8ed6282393263c91283d851924efa178db312b7ee221ed3e1ccc343954348ae52a7b11a3445ab9f92b150b89f0e525a6ff57672add016c languageName: node linkType: hard @@ -8324,7 +8324,7 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^8.31.0" "@typescript-eslint/parser": "npm:^8.31.0" "@use-gesture/react": "npm:^10.2.11" - "@vector-im/compound-design-tokens": "npm:^6.0.0" + "@vector-im/compound-design-tokens": "npm:^8.0.0" "@vector-im/compound-web": "npm:^8.0.0" "@vitejs/plugin-react": "npm:^4.0.1" "@vitest/coverage-v8": "npm:^4.0.18" From 67d7ec670335bc19510a0916a4661b21bea43cc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:02:14 +0000 Subject: [PATCH 14/33] Update dependency lodash-es to v4.18.1 [SECURITY] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cbbbf32f..4f11d9b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11194,9 +11194,9 @@ __metadata: linkType: hard "lodash-es@npm:^4.17.21": - version: 4.17.23 - resolution: "lodash-es@npm:4.17.23" - checksum: 10c0/3150fb6660c14c7a6b5f23bd11597d884b140c0e862a17fdb415aaa5ef7741523182904a6b7929f04e5f60a11edb5a79499eb448734381c99ffb3c4734beeddd + version: 4.18.1 + resolution: "lodash-es@npm:4.18.1" + checksum: 10c0/35d4dcf87ef07f8d090f409447575800108057e360b445f590d0d25d09e3d1e33a163d2fc100d4d072b0f901d5e2fc533cd7c4bfd8eeb38a06abec693823c8b8 languageName: node linkType: hard From fd08489afb8fe58df8b1ed13681913fa67357c56 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 14:32:26 +0200 Subject: [PATCH 15/33] add failing test showing delayId not properly used for delegation --- .../localMember/LocalTransport.test.ts | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index f6c00c7f..fe5a7474 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -19,7 +19,7 @@ import { type CallMembership, type LivekitTransportConfig, } from "matrix-js-sdk/lib/matrixrtc"; -import { BehaviorSubject, lastValueFrom } from "rxjs"; +import { BehaviorSubject, filter, lastValueFrom } from "rxjs"; import fetchMock from "fetch-mock"; import { @@ -28,7 +28,11 @@ import { ownMemberMock, mockRtcMembership, } from "../../../utils/test"; -import { createLocalTransport$, JwtEndpointVersion } from "./LocalTransport"; +import { + createLocalTransport$, + JwtEndpointVersion, + type LocalTransportWithSFUConfig, +} from "./LocalTransport"; import { constant } from "../../Behavior"; import { Epoch, ObservableScope, trackEpoch } from "../../ObservableScope"; import { @@ -539,4 +543,72 @@ describe("LocalTransport", () => { ); }); }); + + it.fails( + "should not update advertised transport on delayID changes, but active should update", + async () => { + // For simplicity, we'll just use the config livekit + customLivekitUrl.setValue("https://lk.example.org"); + + vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue( + openIdResponse, + ); + + const delayId$ = new BehaviorSubject(null); + + const { advertised$, active$ } = createLocalTransport$({ + scope, + ownMembershipIdentity: ownMemberMock, + roomId: "!example_room_id", + // We want multi-sdu + useOldestMember: false, + forceJwtEndpoint: JwtEndpointVersion.Legacy, + delayId$: delayId$, + memberships$: constant(new Epoch([])), + client: { + getDomain: () => "", + baseUrl: "https://example.org", + // eslint-disable-next-line @typescript-eslint/naming-convention + _unstable_getRTCTransports: async () => Promise.resolve([]), + getAccessToken: vi.fn().mockReturnValue("access_token"), + // These won't be called in this error path but satisfy the type + getOpenIdToken: vi.fn(), + getDeviceId: vi.fn(), + }, + }); + + const advertisedValues: LivekitTransportConfig[] = []; + const activeValues: LocalTransportWithSFUConfig[] = []; + advertised$ + .pipe(filter((v) => v !== null)) + .subscribe((t) => advertisedValues.push(t)); + active$ + .pipe(filter((v) => v !== null)) + .subscribe((t) => activeValues.push(t)); + + await flushPromises(); + + // we have now an active and an advertised + expect(advertisedValues.length).toEqual(1); + expect(activeValues.length).toEqual(1); + expect(advertisedValues[0]!.livekit_service_url).toEqual( + "https://lk.example.org", + ); + expect(activeValues[0]!.transport.livekit_service_url).toEqual( + "https://lk.example.org", + ); + + // Now emits 3 new delays id + delayId$.next("delay_id_1"); + await flushPromises(); + delayId$.next("delay_id_2"); + await flushPromises(); + delayId$.next("delay_id_3"); + await flushPromises(); + + // No new emissions should've happened, it is the same transport. only auth and delegation of delay has changed + expect(advertisedValues.length).toEqual(1); + expect(activeValues.length).toEqual(4); + }, + ); }); From c5c154c99b08f63859f5d7eb1a9f739384111a44 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 14:37:25 +0200 Subject: [PATCH 16/33] add self contained domain logic to discover transports --- .../RtcTransportAutoDiscovery.test.ts | 233 ++++++++++++++++++ .../localMember/RtcTransportAutoDiscovery.ts | 172 +++++++++++++ 2 files changed, 405 insertions(+) create mode 100644 src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts create mode 100644 src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts diff --git a/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts new file mode 100644 index 00000000..9314b993 --- /dev/null +++ b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts @@ -0,0 +1,233 @@ +/* +Copyright 2025 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 { + beforeEach, + describe, + expect, + it, + type MockedObject, + vi, +} from "vitest"; +import { type IClientWellKnown, MatrixError } from "matrix-js-sdk"; +import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +import { + type LivekitTransportConfig, + type Transport, +} from "matrix-js-sdk/lib/matrixrtc"; + +import type { ResolvedConfigOptions } from "../../../config/ConfigOptions.ts"; +import { + RtcTransportAutoDiscovery, + type RtcTransportAutoDiscoveryProps, +} from "./RtcTransportAutoDiscovery.ts"; + +type DiscoveryClient = RtcTransportAutoDiscoveryProps["client"]; + +const backendTransport: LivekitTransportConfig = { + type: "livekit", + livekit_service_url: "https://backend.example.org", +}; + +const wellKnownTransport: LivekitTransportConfig = { + type: "livekit", + livekit_service_url: "https://well-known.example.org", +}; + +function makeClient(): MockedObject { + return { + getDomain: vi.fn().mockReturnValue("example.org"), + baseUrl: "https://matrix.example.org", + _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), + getAccessToken: vi.fn().mockReturnValue("access_token"), + getOpenIdToken: vi.fn(), + getDeviceId: vi.fn(), + } as unknown as MockedObject; +} + +function makeResolvedConfig(livekitServiceUrl?: string): ResolvedConfigOptions { + return { + livekit: livekitServiceUrl + ? { + livekit_service_url: livekitServiceUrl, + } + : undefined, + } as ResolvedConfigOptions; +} + +function makeWellKnown(rtcFoci?: Transport[]): IClientWellKnown { + return { + "org.matrix.msc4143.rtc_foci": rtcFoci, + } as unknown as IClientWellKnown; +} + +describe("RtcTransportAutoDiscovery", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + const VALID_TEST_CASES: Array<{ transports: Transport[] }> = [ + { transports: [backendTransport] }, + // will pick the first livekit transport in the list, even if there are other non-livekit transports + { transports: [{ type: "not_livekit" }, backendTransport] }, + ]; + it.each(VALID_TEST_CASES)( + "prefers backend transport over well-known and app config $transports", + async ({ transports }) => { + // it("prefers backend transport over well-known and app config", async () => { + const client = makeClient(); + client._unstable_getRTCTransports.mockResolvedValue(transports); + + const wellKnownFetcher = vi + .fn<(domain: string) => Promise>() + .mockResolvedValue(makeWellKnown([wellKnownTransport])); + + const discovery = new RtcTransportAutoDiscovery({ + client, + resolvedConfig: makeResolvedConfig("https://config.example.org"), + wellKnownFetcher, + logger: rootLogger, + }); + + await expect( + discovery.discoverPreferredTransport(), + ).resolves.toStrictEqual(backendTransport); + + expect(client._unstable_getRTCTransports).toHaveBeenCalledTimes(1); + expect(wellKnownFetcher).not.toHaveBeenCalled(); + }, + ); + + it("Retries limit_exceeded backend transport over well-known", async () => { + const client = makeClient(); + client._unstable_getRTCTransports + .mockRejectedValueOnce( + new MatrixError( + { + errcode: "M_LIMIT_EXCEEDED", + error: "Too many requests", + retry_after_ms: 100, + }, + 429, + ), + ) + .mockResolvedValue([backendTransport]); + + const wellKnownFetcher = vi + .fn<(domain: string) => Promise>() + .mockResolvedValue(makeWellKnown([wellKnownTransport])); + + const discovery = new RtcTransportAutoDiscovery({ + client, + resolvedConfig: makeResolvedConfig("https://config.example.org"), + wellKnownFetcher, + logger: rootLogger, + }); + + await expect(discovery.discoverPreferredTransport()).resolves.toStrictEqual( + backendTransport, + ); + + expect(client._unstable_getRTCTransports).toHaveBeenCalledTimes(2); + expect(wellKnownFetcher).not.toHaveBeenCalled(); + }); + + const INVALID_TEST_CASES: Array<{ transports: Transport[] }> = [ + { transports: [] }, + { transports: [{ type: "not_livekit" }] }, + ]; + it.each(INVALID_TEST_CASES)( + "falls back to well-known when backend has no (valid) livekit transports $transports", + async ({ transports }) => { + const client = makeClient(); + client._unstable_getRTCTransports.mockResolvedValue(transports); + + const wellKnownFetcher = vi + .fn<(domain: string) => Promise>() + .mockResolvedValue(makeWellKnown([wellKnownTransport])); + + const discovery = new RtcTransportAutoDiscovery({ + client, + resolvedConfig: makeResolvedConfig("https://config.example.org"), + wellKnownFetcher, + logger: rootLogger, + }); + + await expect( + discovery.discoverPreferredTransport(), + ).resolves.toStrictEqual(wellKnownTransport); + + expect(wellKnownFetcher).toHaveBeenCalledWith("example.org"); + }, + ); + + it("skips backend discovery in widget mode and uses well-known", async () => { + const client = makeClient(); + // widget mode is detected by the absence of an access token + client.getAccessToken.mockReturnValue(null); + + const wellKnownFetcher = vi + .fn<(domain: string) => Promise>() + .mockResolvedValue(makeWellKnown([wellKnownTransport])); + + const discovery = new RtcTransportAutoDiscovery({ + client, + resolvedConfig: makeResolvedConfig("https://config.example.org"), + wellKnownFetcher, + logger: rootLogger, + }); + + await expect(discovery.discoverPreferredTransport()).resolves.toStrictEqual( + wellKnownTransport, + ); + + expect(client._unstable_getRTCTransports).not.toHaveBeenCalled(); + expect(wellKnownFetcher).toHaveBeenCalledWith("example.org"); + }); + + it("falls back to app config when backend fails and well-known has no rtc_foci", async () => { + const client = makeClient(); + client._unstable_getRTCTransports.mockRejectedValue( + new MatrixError({ errcode: "M_UNKNOWN" }, 404), + ); + + const wellKnownFetcher = vi + .fn<(domain: string) => Promise>() + .mockResolvedValue({} as IClientWellKnown); + + const discovery = new RtcTransportAutoDiscovery({ + client, + resolvedConfig: makeResolvedConfig("https://config.example.org"), + wellKnownFetcher, + logger: rootLogger, + }); + + await expect(discovery.discoverPreferredTransport()).resolves.toStrictEqual( + { + type: "livekit", + livekit_service_url: "https://config.example.org", + }, + ); + }); + + it("returns null when backend, well-known and config are all unavailable", async () => { + const client = makeClient(); + client._unstable_getRTCTransports.mockResolvedValue([]); + + const wellKnownFetcher = vi + .fn<(domain: string) => Promise>() + .mockResolvedValue({} as IClientWellKnown); + + const discovery = new RtcTransportAutoDiscovery({ + client, + resolvedConfig: makeResolvedConfig(undefined), + wellKnownFetcher, + logger: rootLogger, + }); + + await expect(discovery.discoverPreferredTransport()).resolves.toBeNull(); + }); +}); diff --git a/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts new file mode 100644 index 00000000..6d2bac46 --- /dev/null +++ b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts @@ -0,0 +1,172 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-IdFentifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ +import { + isLivekitTransportConfig, + type LivekitTransportConfig, +} from "matrix-js-sdk/lib/matrixrtc"; +import { type IClientWellKnown, type MatrixClient } from "matrix-js-sdk"; +import { type Logger } from "matrix-js-sdk/lib/logger"; + +import type { ResolvedConfigOptions } from "../../../config/ConfigOptions.ts"; +import { doNetworkOperationWithRetry } from "../../../utils/matrix.ts"; + +type TransportDiscoveryClient = Pick< + MatrixClient, + "getDomain" | "_unstable_getRTCTransports" | "getAccessToken" +>; + +export interface RtcTransportAutoDiscoveryProps { + client: TransportDiscoveryClient; + resolvedConfig: ResolvedConfigOptions; + wellKnownFetcher: (domain: string) => Promise; + logger: Logger; +} + +export class RtcTransportAutoDiscovery { + private readonly client: TransportDiscoveryClient; + private readonly resolvedConfig: ResolvedConfigOptions; + private readonly wellKnownFetcher: ( + domain: string, + ) => Promise; + private readonly logger: Logger; + + public constructor({ + client, + resolvedConfig, + wellKnownFetcher, + logger, + }: RtcTransportAutoDiscoveryProps) { + this.client = client; + this.resolvedConfig = resolvedConfig; + this.wellKnownFetcher = wellKnownFetcher; + this.logger = logger.getChild("[RtcTransportAutoDiscovery]"); + } + + public async discoverPreferredTransport(): Promise { + // 1) backend transports + const backendTransport = await this.tryBackendTransports(); + if (backendTransport) { + this.logger.info( + `Found backend transport: ${backendTransport.livekit_service_url}`, + ); + return backendTransport; + } + + this.logger.info("No backend transport found, falling back to well-known"); + // 2) .well-known transports + const wellKnownTransport = await this.tryWellKnownTransports(); + if (wellKnownTransport) { + this.logger.info( + `Found .well-known transport: ${wellKnownTransport.livekit_service_url}`, + ); + return wellKnownTransport; + } + + this.logger.info( + "No .well-known transport found, falling back to app config", + ); + + // 3) app config URL + const configTransport = this.tryConfigTransport(); + if (configTransport) { + this.logger.info( + `Found app config transport: ${configTransport.livekit_service_url}`, + ); + return configTransport; + } + + return null; + } + + /** + * Fetches the first rtc_foci from the backend. + * This will not throw errors, but instead just log them and return null if the expected config is not found or malformed. + * @private + */ + private async tryBackendTransports(): Promise { + const client = this.client; + // MSC4143: Attempt to fetch transports from backend. + // TODO: Workaround for an issue in the js-sdk RoomWidgetClient that + // is not yet implementing _unstable_getRTCTransports properly (via widget API new action). + // For now we just skip this call if we are in a widget. + // In widget mode the client is a `RoomWidgetClient` which has no access token (it is using the widget API). + // Could be removed once the js-sdk is fixed (https://github.com/matrix-org/matrix-js-sdk/issues/5245) + const isSPA = !!client.getAccessToken(); + if (isSPA && "_unstable_getRTCTransports" in client) { + this.logger.info("First try to use getRTCTransports end point ..."); + try { + const transportList = await doNetworkOperationWithRetry(async () => + client._unstable_getRTCTransports(), + ); + const first = transportList.filter(isLivekitTransportConfig)[0]; + if (first) { + return first; + } else { + this.logger.info( + `No livekit transport found in getRTCTransports end point`, + transportList, + ); + } + } catch (ex) { + this.logger.info(`Failed to use getRTCTransports end point: ${ex}`); + } + } else { + this.logger.debug(`getRTCTransports end point not available`); + } + + return null; + } + + /** + * Fetches the first rtc_foci from the .well-known/matrix/client. + * This will not throw errors, but instead just log them and return null if the expected config is not found or malformed. + * @private + */ + private async tryWellKnownTransports(): Promise { + // Legacy MSC4143 (to be removed) WELL_KNOWN: Prioritize the .well-known/matrix/client, if available. + const client = this.client; + const domain = client.getDomain(); + if (domain) { + // we use AutoDiscovery instead of relying on the MatrixClient having already + // been fully configured and started + + const wellKnownFoci = await this.wellKnownFetcher(domain); + + const fociConfig = wellKnownFoci["org.matrix.msc4143.rtc_foci"]; + if (fociConfig) { + if (!Array.isArray(fociConfig)) { + this.logger.warn( + `org.matrix.msc4143.rtc_foci is not an array in .well-known`, + ); + } else { + return fociConfig[0]; + } + } else { + this.logger.info( + `No .well-known "org.matrix.msc4143.rtc_foci" found for ${domain}`, + wellKnownFoci, + ); + } + } else { + // Should never happen, but just in case + this.logger.warn(`No domain configured for client`); + } + + return null; + } + + private tryConfigTransport(): LivekitTransportConfig | null { + const url = this.resolvedConfig.livekit?.livekit_service_url; + if (url) { + return { + type: "livekit", + livekit_service_url: url, + }; + } + return null; + } +} From 6dcb4701628a4d6bbb12b409d2b4c351f646fade Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 14:38:49 +0200 Subject: [PATCH 17/33] Refactor local transport - use the new domain logic to discover the transport - then try to authenticate - Also fix the bug in multi sfu where active$ not updated on delayId change --- src/livekit/openIDSFU.ts | 98 ++-- .../localMember/LocalTransport.test.ts | 2 +- .../localMember/LocalTransport.ts | 417 ++++++------------ 3 files changed, 189 insertions(+), 328 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index d3756e6c..d1f6d451 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { - retryNetworkOperation, - type IOpenIDToken, - type MatrixClient, -} from "matrix-js-sdk"; +import { 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"; @@ -70,6 +66,7 @@ export type OpenIDClientParts = Pick< MatrixClient, "getOpenIdToken" | "getDeviceId" >; + /** * Gets a bearer token from the homeserver and then use it to authenticate * to the matrix RTC backend in order to get acces to the SFU. @@ -113,9 +110,6 @@ export async function getSFUConfigWithOpenID( ); } logger?.debug("Got openID token", openIdToken); - - logger?.info(`Trying to get JWT for focus ${serviceUrl}...`); - let sfuConfig: { url: string; jwt: string } | undefined; const tryBothJwtEndpoints = opts?.forceJwtEndpoint === undefined; // This is for SFUs where we do not publish. @@ -127,7 +121,10 @@ export async function getSFUConfigWithOpenID( // if we can use both or if we are forced to use the new one. if (tryBothJwtEndpoints || forceMatrix2Jwt) { try { - sfuConfig = await getLiveKitJWTWithDelayDelegation( + logger?.info( + `Trying to get JWT with delegation for focus ${serviceUrl}...`, + ); + const sfuConfig = await getLiveKitJWTWithDelayDelegation( membership, serviceUrl, roomId, @@ -135,33 +132,36 @@ export async function getSFUConfigWithOpenID( opts?.delayEndpointBaseUrl, opts?.delayId, ); - logger?.info(`Got JWT from call's active focus URL.`); + + return extractFullConfigFromToken(sfuConfig); } catch (e) { logger?.debug(`Failed fetching jwt with matrix 2.0 endpoint:`, e); - 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: forceOldJwtEndpoint is set to false, we did not get a not supported error from the sfu)`, - e, - ); - // Make this throw a hard error in case we force the matrix2.0 endpoint. - if (forceMatrix2Jwt) - throw new NoMatrix2AuthorizationService(e as Error); - // NEVER get bejond this point if we forceMatrix2 and it failed! - } + // Make this throw a hard error in case we force the matrix2.0 endpoint. + if (forceMatrix2Jwt) throw new NoMatrix2AuthorizationService(e as Error); + + // if (e instanceof NotSupportedError) { + // logger?.warn( + // `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported`, + // e, + // ); + // } else { + // logger?.warn( + // `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, + // ); + // // NEVER get bejond this point if we forceMatrix2 and it failed! + // } } } // DEPRECATED - // here we either have a sfuConfig or we alredy exited because of `if (forceMatrix2) throw ...` + // here we either have a sfuConfig or we already exited because of `if (forceMatrix2) throw ...` // The only case we can get into this condition is, if `forceMatrix2` is `false` - if (sfuConfig === undefined) { + try { + logger?.info( + `Trying to get JWT with legacy endpoint for focus ${serviceUrl}...`, + ); sfuConfig = await getLiveKitJWT( membership.deviceId, serviceUrl, @@ -169,15 +169,19 @@ export async function getSFUConfigWithOpenID( openIdToken, ); logger?.info(`Got JWT from call's active focus URL.`); + return extractFullConfigFromToken(sfuConfig); + } catch (ex) { + throw new FailToGetOpenIdToken( + ex instanceof Error ? ex : new Error(`Unknown error ${ex}`), + ); } +} - if (!sfuConfig) { - throw new Error("No `sfuConfig` after trying with old and new endpoints"); - } - - // Pull the details from the JWT +function extractFullConfigFromToken(sfuConfig: { + url: string; + jwt: string; +}): SFUConfig { 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, @@ -189,16 +193,15 @@ 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 }> { - let res: Response | undefined; - await retryNetworkOperation(RETRIES, async () => { - res = await fetch(livekitServiceURL + "/sfu/get", { + const res = await doNetworkOperationWithRetry(async () => { + return await fetch(livekitServiceURL + "/sfu/get", { method: "POST", headers: { "Content-Type": "application/json", @@ -211,11 +214,7 @@ async function getLiveKitJWT( }), }); }); - 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); } @@ -262,10 +261,8 @@ export async function getLiveKitJWTWithDelayDelegation( }; } - let res: Response | undefined; - - await retryNetworkOperation(RETRIES, async () => { - res = await fetch(livekitServiceURL + "/get_token", { + const res = await doNetworkOperationWithRetry(async () => { + return await fetch(livekitServiceURL + "/get_token", { method: "POST", headers: { "Content-Type": "application/json", @@ -274,11 +271,6 @@ export async function getLiveKitJWTWithDelayDelegation( }); }); - 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) { diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index fe5a7474..165db7dd 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -544,7 +544,7 @@ describe("LocalTransport", () => { }); }); - it.fails( + it( "should not update advertised transport on delayID changes, but active should update", async () => { // For simplicity, we'll just use the config livekit diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index da4fe1dc..76b36cbb 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -8,11 +8,11 @@ Please see LICENSE in the repository root for full details. import { type CallMembership, isLivekitTransportConfig, - type Transport, type LivekitTransportConfig, } from "matrix-js-sdk/lib/matrixrtc"; -import { MatrixError, type MatrixClient } from "matrix-js-sdk"; +import { type MatrixClient } from "matrix-js-sdk"; import { + combineLatest, distinctUntilChanged, first, from, @@ -42,6 +42,7 @@ import { } from "../../../livekit/openIDSFU.ts"; import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts"; import { customLivekitUrl } from "../../../settings/settings.ts"; +import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts"; const logger = rootLogger.getChild("[LocalTransport]"); @@ -137,91 +138,75 @@ export const createLocalTransport$ = ({ forceJwtEndpoint, delayId$, }: Props): LocalTransport => { - /** - * The LiveKit transport in use by the oldest RTC membership. `null` when the - * oldest member has no such transport. - */ - const oldestMemberTransport$ = scope.behavior( - memberships$.pipe( - map((memberships) => { - const oldestMember = memberships.value[0]; - if (oldestMember === undefined) { - logger.info("Oldest member: not found"); - return null; - } - const transport = oldestMember.getTransport(oldestMember); - if (transport === undefined) { - logger.warn( - `Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has no transport`, - ); - return null; - } - if (!isLivekitTransportConfig(transport)) { - logger.warn( - `Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has invalid transport`, - ); - return null; - } - logger.info( - "Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has valid transport", - ); - return transport; - }), - distinctUntilChanged(areLivekitTransportsEqual), - ), + // The LiveKit transport in use by the oldest RTC membership. `null` when the + // oldest member has no such transport. + const oldestMemberTransport$ = observerOldestMembership$(scope, memberships$); + + const transportDiscovery = new RtcTransportAutoDiscovery({ + client: client, + resolvedConfig: Config.get(), + wellKnownFetcher: AutoDiscovery.getRawClientConfig.bind(AutoDiscovery), + logger: logger, + }); + + // Get the preferred transport from the current deployement. + const discoveredTransport$ = from( + transportDiscovery.discoverPreferredTransport(), ); - /** - * The transport that we would personally prefer to publish on (if not for the - * transport preferences of others, perhaps). `null` until fetched and - * validated. - * - * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken - */ - const preferredTransport$ = - scope.behavior( - // preferredTransport$ (used for multi sfu) needs to know if we are using the old or new - // jwt endpoint (`get_token` vs `sfu/get`) based on that the jwt endpoint will compute the rtcBackendIdentity - // differently. (sha(`${userId}|${deviceId}|${memberId}`) vs `${userId}|${deviceId}|${memberId}`) - // When using sticky events (we need to use the new endpoint). - customLivekitUrl.value$.pipe( - switchMap((customUrl) => - startWith(null)( - // Fetch the SFU config, and repeat this asynchronously for every - // change in delay ID. - delayId$.pipe( - switchMap(async (delayId) => { - logger.info( - "Creating preferred transport based on: ", - "customUrl: ", - customUrl, - "delayId: ", - delayId, - "forceJwtEndpoint: ", - forceJwtEndpoint, - ); - return makeTransport( - client, - ownMembershipIdentity, - roomId, - customUrl, - forceJwtEndpoint, - delayId ?? undefined, - ); - }), - // We deliberately hide any changes to the SFU config because we - // do not actually want the app to reconnect whenever the JWT - // token changes due to us delegating a new delayed event. The - // initial SFU config for the transport is all the app needs. - distinctUntilChanged((prev, next) => - areLivekitTransportsEqual(prev.transport, next.transport), - ), - ), - ), - ), - ), + const preferredConfig$ = customLivekitUrl.value$ + .pipe( + startWith(null), + switchMap((customUrl) => { + if (customUrl) { + return of({ + type: "livekit", + livekit_service_url: customUrl, + } as LivekitTransportConfig); + } else { + return discoveredTransport$; + } + }), + ) + .pipe( + map((config) => { + if (!config) { + // Bubbled up from the preferredConfig$ observable. + throw new MatrixRTCTransportMissingError(client.getDomain() ?? ""); + } + return config; + }), + distinctUntilChanged(areLivekitTransportsEqual), ); + const preferredTransport$ = combineLatest([preferredConfig$, delayId$]).pipe( + switchMap(async ([transport, delayId]) => { + try { + return await doOpenIdAndJWTFromUrl( + transport.livekit_service_url, + forceJwtEndpoint, + ownMembershipIdentity, + roomId, + client, + delayId ?? undefined, + ); + } catch (e) { + if ( + e instanceof FailToGetOpenIdToken || + e instanceof NoMatrix2AuthorizationService + ) { + // rethrow as is + throw e; + } + // Catch others and rethrow as FailToGetOpenIdToken that has user friendly message. + logger.error("Failed to get JWT from preferred transport", e); + throw new FailToGetOpenIdToken( + e instanceof Error ? e : new Error(String(e)), + ); + } + }), + ); + if (useOldestMember) { // --- Oldest member mode --- return { @@ -232,7 +217,7 @@ export const createLocalTransport$ = ({ advertised$: scope.behavior( merge( oldestMemberTransport$, - preferredTransport$.pipe(map((t) => t?.transport ?? null)), + preferredTransport$.pipe(map((t) => t.transport)), ).pipe( first((t) => t !== null), tap((t) => @@ -268,6 +253,7 @@ export const createLocalTransport$ = ({ ), ), ), + null, ), }; } @@ -277,210 +263,93 @@ export const createLocalTransport$ = ({ return { advertised$: scope.behavior( preferredTransport$.pipe( - map((t) => t?.transport ?? null), + map((t) => t.transport), distinctUntilChanged(areLivekitTransportsEqual), ), + null, ), - active$: preferredTransport$, + active$: scope.behavior(preferredTransport$, null), }; }; -const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; - /** - * Determine the correct Transport for the current session, including - * validating auth against the service to ensure it's correct. - * Prefers in order: - * - - * 1. The `urlFromDevSettings` value. If this cannot be validated, the function will throw. - * 2. The transports returned via the homeserver. - * 3. The transports returned via .well-known. - * 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 forceJwtEndpoint Whether to force a specific JWT endpoint - * - `Legacy` / `Matrix_2_0` - * - `get_token` / `sfu/get` - * - not hashing / hashing the backendIdentity - * @param delayId the delay id passed to the jwt service. - * - * @returns A fully validated transport config. - * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken + * Observes the oldest member in the room and returns the transport that it uses if it is a livekit transport. + * @param scope - The observable scope. + * @param memberships$ - The observable of the call's memberships.' */ -async function makeTransport( +function observerOldestMembership$( + scope: ObservableScope, + memberships$: Behavior>, +): Behavior { + return scope.behavior( + memberships$.pipe( + map((memberships) => { + const oldestMember = memberships.value[0]; + if (oldestMember === undefined) { + logger.info("Oldest member: not found"); + return null; + } + const transport = oldestMember.getTransport(oldestMember); + if (transport === undefined) { + logger.warn( + `Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has no transport`, + ); + return null; + } + if (!isLivekitTransportConfig(transport)) { + logger.warn( + `Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has invalid transport`, + ); + return null; + } + logger.info( + "Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has valid transport", + ); + return transport; + }), + distinctUntilChanged(areLivekitTransportsEqual), + ), + ); +} + +// Utility to ensure the user can authenticate with the SFU. +// +// We will call `getSFUConfigWithOpenID` once per transport here as it's our +// only mechanism of validation. This means we will also ask the +// homeserver for a OpenID token a few times. Since OpenID tokens are single +// use we don't want to risk any issues by re-using a token. +// +// If the OpenID request were to fail, then it's acceptable for us to fail +// this function early, as we assume the homeserver has got some problems. +async function doOpenIdAndJWTFromUrl( + url: string, + forceJwtEndpoint: JwtEndpointVersion, + membership: CallMembershipIdentityParts, + roomId: string, client: Pick< MatrixClient, "getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken" > & OpenIDClientParts, - membership: CallMembershipIdentityParts, - roomId: string, - urlFromDevSettings: string | null, - forceJwtEndpoint: JwtEndpointVersion, delayId?: string, ): Promise { - logger.trace("Searching for a preferred transport"); - - async function doOpenIdAndJWTFromUrl( - url: string, - ): Promise { - const sfuConfig = await getSFUConfigWithOpenID( - client, - membership, - url, - roomId, - { - forceJwtEndpoint: forceJwtEndpoint, - delayEndpointBaseUrl: client.baseUrl, - delayId, - }, - logger, - ); - return { - transport: { - type: "livekit", - livekit_service_url: url, - }, - sfuConfig, - }; - } - // 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 - // use we don't want to risk any issues by re-using a token. - // - // If the OpenID request were to fail then it's acceptable for us to fail - // this function early, as we assume the homeserver has got some problems. - - // DEVTOOL: Highest priority: Load from devtool setting - if (urlFromDevSettings !== null) { - // 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. - logger.info("Using LiveKit transport from dev tools: ", urlFromDevSettings); - return await doOpenIdAndJWTFromUrl(urlFromDevSettings); - } - - async function getFirstUsableTransport( - transports: Transport[], - ): Promise { - for (const potentialTransport of transports) { - if (isLivekitTransportConfig(potentialTransport)) { - try { - logger.info( - `makeTransport: check transport authentication for "${potentialTransport.livekit_service_url}"`, - ); - // This will call the jwt/sfu/get endpoint to pre create the livekit room. - return await doOpenIdAndJWTFromUrl( - potentialTransport.livekit_service_url, - ); - } catch (ex) { - logger.debug( - `makeTransport: Could not use SFU service "${potentialTransport.livekit_service_url}" as SFU`, - ex, - ); - // Explictly throw these - if (ex instanceof FailToGetOpenIdToken) { - throw ex; - } - if (ex instanceof NoMatrix2AuthorizationService) { - throw ex; - } - } - } else { - logger.info( - `makeTransport: "${potentialTransport.livekit_service_url}" is not a valid livekit transport as SFU`, - ); - } - } - return null; - } - - // MSC4143: Attempt to fetch transports from backend. - // TODO: Workaround for an issue in the js-sdk RoomWidgetClient that - // is not yet implementing _unstable_getRTCTransports properly (via widget API new action). - // For now we just skip this call if we are in a widget. - // In widget mode the client is a `RoomWidgetClient` which has no access token (it is using the widget API). - // Could be removed once the js-sdk is fixed (https://github.com/matrix-org/matrix-js-sdk/issues/5245) - const isSPA = !!client.getAccessToken(); - if (isSPA && "_unstable_getRTCTransports" in client) { - logger.info( - "makeTransport: First try to use getRTCTransports end point ...", - ); - try { - // TODO This should also check for server support? - const transportList = await client._unstable_getRTCTransports(); - const selectedTransport = await getFirstUsableTransport(transportList); - if (selectedTransport) { - logger.info( - "makeTransport: ...Using backend-configured (client.getRTCTransports) SFU", - selectedTransport, - ); - return selectedTransport; - } - } catch (ex) { - if (ex instanceof MatrixError && ex.httpStatus === 404) { - // Expected, this is an unstable endpoint and it's not required. - // There will be expected 404 errors in the console. When we check if synapse supports the endpoint. - logger.debug( - "Matrix homeserver does not provide any RTC transports via `/rtc/transports` (will retry with well-known.)", - ); - } else if (ex instanceof FailToGetOpenIdToken) { - throw ex; - } else { - // We got an error that wasn't just missing support for the feature, so log it loudly. - logger.error( - "Unexpected error fetching RTC transports from backend", - ex, - ); - } - } - } - - logger.info( - `makeTransport: Trying to get transports from .well-known/matrix/client on domain ${client.getDomain()} ...`, + const sfuConfig = await getSFUConfigWithOpenID( + client, + membership, + url, + roomId, + { + forceJwtEndpoint: forceJwtEndpoint, + delayEndpointBaseUrl: client.baseUrl, + delayId, + }, + logger, ); - - // Legacy MSC4143 (to be removed) WELL_KNOWN: Prioritize the .well-known/matrix/client, if available. - const domain = client.getDomain(); - if (domain) { - // we use AutoDiscovery instead of relying on the MatrixClient having already - // been fully configured and started - const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[ - FOCI_WK_KEY - ]; - const selectedTransport = Array.isArray(wellKnownFoci) - ? await getFirstUsableTransport(wellKnownFoci) - : null; - if (selectedTransport) { - logger.info("Using .well-known SFU", selectedTransport); - return selectedTransport; - } - } - - logger.info( - `makeTransport: No valid transport found via backend or .well-known, falling back to config if available.`, - ); - - // CONFIG: Least prioritized; Load from config file - 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. - logger.info("Using config SFU", urlFromConf); - return await doOpenIdAndJWTFromUrl(urlFromConf); - } catch (ex) { - if (ex instanceof FailToGetOpenIdToken) { - throw ex; - } - logger.error("Failed to validate config SFU", ex); - } - } - - // If we do not have returned a transport by now we throw an error - throw new MatrixRTCTransportMissingError(domain ?? ""); + return { + transport: { + type: "livekit", + livekit_service_url: url, + }, + sfuConfig, + }; } From 23f846a308792a9ab21b0c486329015aa72eaec0 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 14:47:34 +0200 Subject: [PATCH 18/33] fixup: test prettier --- .../localMember/LocalTransport.test.ts | 115 +++++++++--------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 165db7dd..aa6228c9 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -544,71 +544,68 @@ describe("LocalTransport", () => { }); }); - it( - "should not update advertised transport on delayID changes, but active should update", - async () => { - // For simplicity, we'll just use the config livekit - customLivekitUrl.setValue("https://lk.example.org"); + it("should not update advertised transport on delayID changes, but active should update", async () => { + // For simplicity, we'll just use the config livekit + customLivekitUrl.setValue("https://lk.example.org"); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue( - openIdResponse, - ); + vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue( + openIdResponse, + ); - const delayId$ = new BehaviorSubject(null); + const delayId$ = new BehaviorSubject(null); - const { advertised$, active$ } = createLocalTransport$({ - scope, - ownMembershipIdentity: ownMemberMock, - roomId: "!example_room_id", - // We want multi-sdu - useOldestMember: false, - forceJwtEndpoint: JwtEndpointVersion.Legacy, - delayId$: delayId$, - memberships$: constant(new Epoch([])), - client: { - getDomain: () => "", - baseUrl: "https://example.org", - // eslint-disable-next-line @typescript-eslint/naming-convention - _unstable_getRTCTransports: async () => Promise.resolve([]), - getAccessToken: vi.fn().mockReturnValue("access_token"), - // These won't be called in this error path but satisfy the type - getOpenIdToken: vi.fn(), - getDeviceId: vi.fn(), - }, - }); + const { advertised$, active$ } = createLocalTransport$({ + scope, + ownMembershipIdentity: ownMemberMock, + roomId: "!example_room_id", + // We want multi-sdu + useOldestMember: false, + forceJwtEndpoint: JwtEndpointVersion.Legacy, + delayId$: delayId$, + memberships$: constant(new Epoch([])), + client: { + getDomain: () => "", + baseUrl: "https://example.org", + // eslint-disable-next-line @typescript-eslint/naming-convention + _unstable_getRTCTransports: async () => Promise.resolve([]), + getAccessToken: vi.fn().mockReturnValue("access_token"), + // These won't be called in this error path but satisfy the type + getOpenIdToken: vi.fn(), + getDeviceId: vi.fn(), + }, + }); - const advertisedValues: LivekitTransportConfig[] = []; - const activeValues: LocalTransportWithSFUConfig[] = []; - advertised$ - .pipe(filter((v) => v !== null)) - .subscribe((t) => advertisedValues.push(t)); - active$ - .pipe(filter((v) => v !== null)) - .subscribe((t) => activeValues.push(t)); + const advertisedValues: LivekitTransportConfig[] = []; + const activeValues: LocalTransportWithSFUConfig[] = []; + advertised$ + .pipe(filter((v) => v !== null)) + .subscribe((t) => advertisedValues.push(t)); + active$ + .pipe(filter((v) => v !== null)) + .subscribe((t) => activeValues.push(t)); - await flushPromises(); + await flushPromises(); - // we have now an active and an advertised - expect(advertisedValues.length).toEqual(1); - expect(activeValues.length).toEqual(1); - expect(advertisedValues[0]!.livekit_service_url).toEqual( - "https://lk.example.org", - ); - expect(activeValues[0]!.transport.livekit_service_url).toEqual( - "https://lk.example.org", - ); + // we have now an active and an advertised + expect(advertisedValues.length).toEqual(1); + expect(activeValues.length).toEqual(1); + expect(advertisedValues[0]!.livekit_service_url).toEqual( + "https://lk.example.org", + ); + expect(activeValues[0]!.transport.livekit_service_url).toEqual( + "https://lk.example.org", + ); - // Now emits 3 new delays id - delayId$.next("delay_id_1"); - await flushPromises(); - delayId$.next("delay_id_2"); - await flushPromises(); - delayId$.next("delay_id_3"); - await flushPromises(); + // Now emits 3 new delays id + delayId$.next("delay_id_1"); + await flushPromises(); + delayId$.next("delay_id_2"); + await flushPromises(); + delayId$.next("delay_id_3"); + await flushPromises(); - // No new emissions should've happened, it is the same transport. only auth and delegation of delay has changed - expect(advertisedValues.length).toEqual(1); - expect(activeValues.length).toEqual(4); - }, - ); + // No new emissions should've happened, it is the same transport. only auth and delegation of delay has changed + expect(advertisedValues.length).toEqual(1); + expect(activeValues.length).toEqual(4); + }); }); From 869bab8253859dbaa667996ad56ebd45cdc9d852 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 16:51:40 +0200 Subject: [PATCH 19/33] fixup test since usage of doNetworkOperationWithRetry doNetworkOperationWithRetry requires to properly mock the get_token endpoint, whereas retryNetworkOperation didn't require it. --- .../CallViewModel/remoteMembers/Connection.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 2c89eef5..7ad5d775 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details. import { afterEach, + beforeEach, describe, expect, it, @@ -151,6 +152,19 @@ afterEach(() => { }); describe("Start connection states", () => { + beforeEach(() => { + fetchMock.post( + `https://matrix-rtc.example.org/livekit/jwt/get_token`, + () => { + return { + // Return a non-retryable error, if not, the retry logic will + // wait and fail the test with a timeout. + status: 404, + }; + }, + ); + }); + it("start in initialized state", () => { setupTest(); From 4cecdda955b82d33b6fbb03047201586dc99954e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 14:48:15 +0200 Subject: [PATCH 20/33] refact: step 1 - extract oldest membership transport into new function --- .../localMember/LocalTransport.ts | 119 +++++++++++------- 1 file changed, 71 insertions(+), 48 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 76b36cbb..425ebe6b 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -18,6 +18,7 @@ import { from, map, merge, + Observable, of, startWith, switchMap, @@ -208,54 +209,14 @@ export const createLocalTransport$ = ({ ); if (useOldestMember) { - // --- Oldest member mode --- - return { - // Never update the transport that we advertise in our membership. Just - // take the first valid oldest member or preferred transport that we learn - // about, and stick with that. This avoids unnecessary SFU hops and room - // state changes. - advertised$: scope.behavior( - merge( - oldestMemberTransport$, - preferredTransport$.pipe(map((t) => t.transport)), - ).pipe( - first((t) => t !== null), - tap((t) => - logger.info(`Advertise transport: ${t.livekit_service_url}`), - ), - ), - null, - ), - // Publish on the transport used by the oldest member. - active$: scope.behavior( - oldestMemberTransport$.pipe( - switchMap((transport) => { - // Oldest member not available (or invalid SFU config). - if (transport === null) return of(null); - // Oldest member available: fetch the SFU config. - const fetchOldestMemberTransport = - async (): Promise => ({ - transport, - sfuConfig: await getSFUConfigWithOpenID( - client, - ownMembershipIdentity, - transport.livekit_service_url, - roomId, - { forceJwtEndpoint: JwtEndpointVersion.Legacy }, - logger, - ), - }); - return from(fetchOldestMemberTransport()).pipe(startWith(null)); - }), - tap((t) => - logger.info( - `Publish on transport: ${t?.transport.livekit_service_url}`, - ), - ), - ), - null, - ), - }; + return observeLocalTransportForOldestMembership( + scope, + oldestMemberTransport$, + preferredTransport$, + client, + ownMembershipIdentity, + roomId, + ); } // --- Multi-SFU mode --- @@ -353,3 +314,65 @@ async function doOpenIdAndJWTFromUrl( sfuConfig, }; } + +function observeLocalTransportForOldestMembership( + scope: ObservableScope, + oldestMemberTransport$: Behavior, + preferredTransport$: Observable, + client: Pick< + MatrixClient, + "getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken" + > & + OpenIDClientParts, + ownMembershipIdentity: CallMembershipIdentityParts, + roomId: string, +): LocalTransport { + // --- Oldest member mode --- + return { + // Never update the transport that we advertise in our membership. Just + // take the first valid oldest member or preferred transport that we learn + // about, and stick with that. This avoids unnecessary SFU hops and room + // state changes. + advertised$: scope.behavior( + merge( + oldestMemberTransport$, + preferredTransport$.pipe(map((t) => t.transport)), + ).pipe( + first((t) => t !== null), + tap((t) => + logger.info(`Advertise transport: ${t.livekit_service_url}`), + ), + ), + null, + ), + // Publish on the transport used by the oldest member. + active$: scope.behavior( + oldestMemberTransport$.pipe( + switchMap((transport) => { + // Oldest member not available (or invalid SFU config). + if (transport === null) return of(null); + // Oldest member available: fetch the SFU config. + const fetchOldestMemberTransport = + async (): Promise => ({ + transport, + sfuConfig: await getSFUConfigWithOpenID( + client, + ownMembershipIdentity, + transport.livekit_service_url, + roomId, + { forceJwtEndpoint: JwtEndpointVersion.Legacy }, + logger, + ), + }); + return from(fetchOldestMemberTransport()).pipe(startWith(null)); + }), + tap((t) => + logger.info( + `Publish on transport: ${t?.transport.livekit_service_url}`, + ), + ), + ), + null, + ), + }; +} From 90bfaecd13a09870e92a45cada774873682a7fe5 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 16:09:18 +0200 Subject: [PATCH 21/33] refact: step 2 - break down old membership local transport logic --- .../localMember/LocalTransport.ts | 98 +++++++++++-------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 425ebe6b..a4f9d9c8 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -12,13 +12,14 @@ import { } from "matrix-js-sdk/lib/matrixrtc"; import { type MatrixClient } from "matrix-js-sdk"; import { + catchError, combineLatest, distinctUntilChanged, first, from, map, merge, - Observable, + type Observable, of, startWith, switchMap, @@ -184,7 +185,7 @@ export const createLocalTransport$ = ({ switchMap(async ([transport, delayId]) => { try { return await doOpenIdAndJWTFromUrl( - transport.livekit_service_url, + transport, forceJwtEndpoint, ownMembershipIdentity, roomId, @@ -192,18 +193,11 @@ export const createLocalTransport$ = ({ delayId ?? undefined, ); } catch (e) { - if ( - e instanceof FailToGetOpenIdToken || - e instanceof NoMatrix2AuthorizationService - ) { - // rethrow as is - throw e; - } - // Catch others and rethrow as FailToGetOpenIdToken that has user friendly message. - logger.error("Failed to get JWT from preferred transport", e); - throw new FailToGetOpenIdToken( - e instanceof Error ? e : new Error(String(e)), + logger.error( + `Failed to authenticate to transport ${transport.livekit_service_url}`, + e, ); + throw mapAuthErrorToUserFriendlyError(e); } }), ); @@ -283,7 +277,7 @@ function observerOldestMembership$( // If the OpenID request were to fail, then it's acceptable for us to fail // this function early, as we assume the homeserver has got some problems. async function doOpenIdAndJWTFromUrl( - url: string, + transport: LivekitTransportConfig, forceJwtEndpoint: JwtEndpointVersion, membership: CallMembershipIdentityParts, roomId: string, @@ -297,7 +291,7 @@ async function doOpenIdAndJWTFromUrl( const sfuConfig = await getSFUConfigWithOpenID( client, membership, - url, + transport.livekit_service_url, roomId, { forceJwtEndpoint: forceJwtEndpoint, @@ -307,10 +301,7 @@ async function doOpenIdAndJWTFromUrl( logger, ); return { - transport: { - type: "livekit", - livekit_service_url: url, - }, + transport, sfuConfig, }; } @@ -327,6 +318,37 @@ function observeLocalTransportForOldestMembership( ownMembershipIdentity: CallMembershipIdentityParts, roomId: string, ): LocalTransport { + // Ensure we can authenticate with the SFU. + const authenticatedOldestMemberTransport$ = oldestMemberTransport$.pipe( + switchMap((transport) => { + // Oldest member not available -we are first- (or invalid SFU config). + if (transport === null) return of(null); + + // Whenever there is transport change we want to revert + // to no transport while we do the authentication. + // So do a from(promise) here to be able to startWith(null) + return from( + doOpenIdAndJWTFromUrl( + transport, + JwtEndpointVersion.Legacy, + ownMembershipIdentity, + roomId, + client, + undefined, + ), + ).pipe( + catchError((e: unknown) => { + logger.error( + `Failed to authenticate to transport ${transport.livekit_service_url}`, + e, + ); + throw mapAuthErrorToUserFriendlyError(e); + }), + startWith(null), + ); + }), + ); + // --- Oldest member mode --- return { // Never update the transport that we advertise in our membership. Just @@ -335,7 +357,9 @@ function observeLocalTransportForOldestMembership( // state changes. advertised$: scope.behavior( merge( - oldestMemberTransport$, + authenticatedOldestMemberTransport$.pipe( + map((t) => t?.transport ?? null), + ), preferredTransport$.pipe(map((t) => t.transport)), ).pipe( first((t) => t !== null), @@ -347,25 +371,7 @@ function observeLocalTransportForOldestMembership( ), // Publish on the transport used by the oldest member. active$: scope.behavior( - oldestMemberTransport$.pipe( - switchMap((transport) => { - // Oldest member not available (or invalid SFU config). - if (transport === null) return of(null); - // Oldest member available: fetch the SFU config. - const fetchOldestMemberTransport = - async (): Promise => ({ - transport, - sfuConfig: await getSFUConfigWithOpenID( - client, - ownMembershipIdentity, - transport.livekit_service_url, - roomId, - { forceJwtEndpoint: JwtEndpointVersion.Legacy }, - logger, - ), - }); - return from(fetchOldestMemberTransport()).pipe(startWith(null)); - }), + authenticatedOldestMemberTransport$.pipe( tap((t) => logger.info( `Publish on transport: ${t?.transport.livekit_service_url}`, @@ -376,3 +382,17 @@ function observeLocalTransportForOldestMembership( ), }; } + +function mapAuthErrorToUserFriendlyError(e: unknown): Error { + if ( + e instanceof FailToGetOpenIdToken || + e instanceof NoMatrix2AuthorizationService + ) { + // rethrow as is + return e; + } + // Catch others and rethrow as FailToGetOpenIdToken that has user friendly message. + return new FailToGetOpenIdToken( + e instanceof Error ? e : new Error(String(e)), + ); +} From b5be123953d6d75251ff770c828ef9a9bfbbedc2 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 16:53:21 +0200 Subject: [PATCH 22/33] fix uncaught errors in tests --- src/state/CallViewModel/CallViewModelTestUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/state/CallViewModel/CallViewModelTestUtils.ts b/src/state/CallViewModel/CallViewModelTestUtils.ts index b6bf8a9a..09a43fc3 100644 --- a/src/state/CallViewModel/CallViewModelTestUtils.ts +++ b/src/state/CallViewModel/CallViewModelTestUtils.ts @@ -131,6 +131,9 @@ export function withCallViewModel(mode: MatrixRTCMode) { public getSyncState(): SyncState { return syncState; } + public getAccessToken(): string | null { + return "a-token"; + } })() as Partial as MatrixClient, getMembers: () => roomMembers, getMembersWithMembership: () => roomMembers, From 95eb8943af63bbf4212f8c6933b35d5ea8b7f341 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 19:05:24 +0200 Subject: [PATCH 23/33] fixup test: Errors are now wrapped in user readable error and proper fetchmock for doNetworkOperationWithRetry --- src/livekit/openIDSFU.test.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/livekit/openIDSFU.test.ts b/src/livekit/openIDSFU.test.ts index 20820748..fc0b6d54 100644 --- a/src/livekit/openIDSFU.test.ts +++ b/src/livekit/openIDSFU.test.ts @@ -19,12 +19,14 @@ import fetchMock from "fetch-mock"; import { getSFUConfigWithOpenID, type OpenIDClientParts } from "./openIDSFU"; import { testJWTToken } from "../utils/test-fixtures"; import { ownMemberMock } from "../utils/test"; +import { FailToGetOpenIdToken } from "../utils/errors"; const sfuUrl = "https://sfu.example.org"; describe("getSFUConfigWithOpenID", () => { let matrixClient: MockedObject; beforeEach(() => { + fetchMock.catch(404); matrixClient = { getOpenIdToken: vitest.fn(), getDeviceId: vitest.fn(), @@ -71,9 +73,10 @@ describe("getSFUConfigWithOpenID", () => { "https://sfu.example.org", "!example_room_id", ); - } catch (ex) { - expect((ex as Error).message).toEqual( - "SFU Config fetch failed with status code 500", + } catch (ex: unknown) { + expect(ex).toBeInstanceOf(FailToGetOpenIdToken); + expect((ex as FailToGetOpenIdToken).cause).toEqual( + new Error("SFU Config fetch failed with status code 500"), ); void (await fetchMock.flush()); return; @@ -106,8 +109,9 @@ describe("getSFUConfigWithOpenID", () => { }, ); } catch (ex) { - expect((ex as Error).message).toEqual( - "SFU Config fetch failed with status code 500", + expect(ex).toBeInstanceOf(FailToGetOpenIdToken); + expect((ex as FailToGetOpenIdToken).cause).toEqual( + new Error("SFU Config fetch failed with status code 500"), ); void (await fetchMock.flush()); } @@ -160,8 +164,9 @@ describe("getSFUConfigWithOpenID", () => { }, ); } catch (ex) { - expect((ex as Error).message).toEqual( - "SFU Config fetch failed with status code 500", + expect(ex).toBeInstanceOf(FailToGetOpenIdToken); + expect((ex as FailToGetOpenIdToken).cause).toEqual( + new Error("SFU Config fetch failed with status code 500"), ); void (await fetchMock.flush()); } From b1f84a34a82c3ed0d3f672a6ceb23403a5cf7771 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2026 20:42:09 +0200 Subject: [PATCH 24/33] remove dead commented code --- src/livekit/openIDSFU.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index d1f6d451..dfe04323 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -137,21 +137,9 @@ export async function getSFUConfigWithOpenID( } catch (e) { logger?.debug(`Failed fetching jwt with matrix 2.0 endpoint:`, e); // Make this throw a hard error in case we force the matrix2.0 endpoint. - if (forceMatrix2Jwt) throw new NoMatrix2AuthorizationService(e as Error); - - // if (e instanceof NotSupportedError) { - // logger?.warn( - // `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported`, - // e, - // ); - // } else { - // logger?.warn( - // `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, - // ); - // // NEVER get bejond this point if we forceMatrix2 and it failed! - // } + if (forceMatrix2Jwt) { + throw new NoMatrix2AuthorizationService(e as Error); + } } } From 3d286e8ab4c078e6c8628d4903efe26f28e2dd83 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2026 09:45:59 +0200 Subject: [PATCH 25/33] revert workaround for widget permission dialog, not needed anymore --- playwright/widget/test-helpers.ts | 25 ------------------------- playwright/widget/voice-call-dm.spec.ts | 4 ---- 2 files changed, 29 deletions(-) diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 31247e1a..ff5fa7e2 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -34,9 +34,6 @@ export class TestHelpers { ).toBeVisible(); await page.getByRole("menuitem", { name: "Element Call" }).click(); - - // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 - await this.dismissFileDialogPermissionIfNeeded(page); } public static async joinCallFromLobby(page: Page): Promise { @@ -63,9 +60,6 @@ export class TestHelpers { await expect(page.getByText(label)).toBeVisible(); await expect(page.getByRole("button", { name: "Join" })).toBeVisible(); await page.getByRole("button", { name: "Join" }).click(); - - // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 - await this.dismissFileDialogPermissionIfNeeded(page); } /** @@ -242,29 +236,10 @@ export class TestHelpers { await page.getByRole("button", { name: "Video call" }).click(); await page.getByRole("menuitem", { name: "Element Call" }).click(); - // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 - await this.dismissFileDialogPermissionIfNeeded(page); - await TestHelpers.setEmbeddedElementCallRtcMode(page, mode); await page.getByRole("button", { name: "Close lobby" }).click(); } - // TODO: Remove as soon as web merges https://github.com/element-hq/element-web/pull/32755 - public static async dismissFileDialogPermissionIfNeeded( - page: Page, - ): Promise { - const dialogHeading = page.getByRole("heading", { - name: "Approve widget permissions", - }); - - try { - await expect(dialogHeading).toBeVisible({ timeout: 3000 }); - await page.getByRole("button", { name: "Approve" }).click(); - } catch { - // Dialog did not appear, that's fine - } - } - /** * Goes to the settings to set the RTC mode. * then closes the settings modal. diff --git a/playwright/widget/voice-call-dm.spec.ts b/playwright/widget/voice-call-dm.spec.ts index f5152e1f..a4e6255b 100644 --- a/playwright/widget/voice-call-dm.spec.ts +++ b/playwright/widget/voice-call-dm.spec.ts @@ -45,8 +45,6 @@ widgetTest( await expect(whistler.page.getByText("Incoming voice call")).toBeVisible(); await whistler.page.getByRole("button", { name: "Accept" }).click(); - await TestHelpers.dismissFileDialogPermissionIfNeeded(whistler.page); - await expect( whistler.page.locator('iframe[title="Element Call"]'), ).toBeVisible(); @@ -140,8 +138,6 @@ widgetTest( await expect(whistler.page.getByText("Incoming video call")).toBeVisible(); await whistler.page.getByRole("button", { name: "Accept" }).click(); - await TestHelpers.dismissFileDialogPermissionIfNeeded(whistler.page); - await expect( whistler.page.locator('iframe[title="Element Call"]'), ).toBeVisible(); From 55eea7a968b96b0b0ee1096512c19cafb929465c Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 3 Apr 2026 17:48:42 +0200 Subject: [PATCH 26/33] Fix type error --- src/TranslatedError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TranslatedError.ts b/src/TranslatedError.ts index 6ffed4a9..32f4ed2e 100644 --- a/src/TranslatedError.ts +++ b/src/TranslatedError.ts @@ -25,7 +25,7 @@ export abstract class TranslatedError extends Error { messageKey: ParseKeys, translationFn: TFunction, ) { - super(translationFn(messageKey, { lng: "en" } as TOptions)); + super(translationFn(messageKey, { lng: "en" })); this.translatedMessage = translationFn(messageKey); } } From 6d99450d81378ad1504859c86d617781a1b3dc47 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2026 18:46:49 +0200 Subject: [PATCH 27/33] review: Remove un-needed initial value for behavior --- src/state/CallViewModel/localMember/LocalTransport.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index a4f9d9c8..40875304 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -158,7 +158,6 @@ export const createLocalTransport$ = ({ const preferredConfig$ = customLivekitUrl.value$ .pipe( - startWith(null), switchMap((customUrl) => { if (customUrl) { return of({ From a89d231858efe1e6e28c4c254a90f82c8fc78214 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2026 18:51:14 +0200 Subject: [PATCH 28/33] review change comment to proper doc --- .../localMember/LocalTransport.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 40875304..f4cbd84a 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -151,7 +151,7 @@ export const createLocalTransport$ = ({ logger: logger, }); - // Get the preferred transport from the current deployement. + // Get the preferred transport from the current deployment. const discoveredTransport$ = from( transportDiscovery.discoverPreferredTransport(), ); @@ -266,15 +266,22 @@ function observerOldestMembership$( ); } -// Utility to ensure the user can authenticate with the SFU. -// -// We will call `getSFUConfigWithOpenID` once per transport here as it's our -// only mechanism of validation. This means we will also ask the -// homeserver for a OpenID token a few times. Since OpenID tokens are single -// use we don't want to risk any issues by re-using a token. -// -// If the OpenID request were to fail, then it's acceptable for us to fail -// this function early, as we assume the homeserver has got some problems. +/** + * Utility to ensure the user can authenticate with the SFU. + * We will call `getSFUConfigWithOpenID` once per transport here as it's our + * only mechanism of validation. This means we will also ask the + * homeserver for a OpenID token a few times. Since OpenID tokens are single + * use we don't want to risk any issues by re-using a token. + * + * @param transport The transport to authenticate with. + * @param forceJwtEndpoint Whether to force the JWT endpoint to be used. + * @param membership The identity of the local member. + * @param roomId The room ID to use for the JWT. + * @param client The client to use for the OpenID token. + * @param delayId The delayId to use for the JWT. + * + * @throws FailToGetOpenIdToken, NoMatrix2AuthorizationService + */ async function doOpenIdAndJWTFromUrl( transport: LivekitTransportConfig, forceJwtEndpoint: JwtEndpointVersion, From 9fe35ba8220f84bec46b50cd5f5a514a9422444e Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2026 19:08:52 +0200 Subject: [PATCH 29/33] Add back the patch to avoid reconnect glitch --- .../localMember/LocalTransport.test.ts | 29 +++++++++++++++---- .../localMember/LocalTransport.ts | 16 +++++++++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index aa6228c9..895f3073 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -544,13 +544,13 @@ describe("LocalTransport", () => { }); }); - it("should not update advertised transport on delayID changes, but active should update", async () => { + it("should not update advertised/active transport on delayID changes, but delay Id delegation should be called", async () => { // For simplicity, we'll just use the config livekit customLivekitUrl.setValue("https://lk.example.org"); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue( - openIdResponse, - ); + const authCallSpy = vi + .spyOn(openIDSFU, "getSFUConfigWithOpenID") + .mockResolvedValue(openIdResponse); const delayId$ = new BehaviorSubject(null); @@ -596,6 +596,7 @@ describe("LocalTransport", () => { "https://lk.example.org", ); + expect(authCallSpy).toHaveBeenCalledTimes(2); // Now emits 3 new delays id delayId$.next("delay_id_1"); await flushPromises(); @@ -604,8 +605,24 @@ describe("LocalTransport", () => { delayId$.next("delay_id_3"); await flushPromises(); - // No new emissions should've happened, it is the same transport. only auth and delegation of delay has changed + // No new emissions should've happened, it is the same transport. expect(advertisedValues.length).toEqual(1); - expect(activeValues.length).toEqual(4); + expect(activeValues.length).toEqual(1); + + // Still we should have updated the delayID to auth + expect(authCallSpy).toHaveBeenCalledTimes( + 4 * 2 /* 2 calls for each delayId ?? why */, + ); + + expect(authCallSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ + delayId: "delay_id_3", + }), + expect.anything(), + ); }); }); diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index f4cbd84a..037b6a0b 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -222,7 +222,21 @@ export const createLocalTransport$ = ({ ), null, ), - active$: scope.behavior(preferredTransport$, null), + active$: scope.behavior( + preferredTransport$.pipe( + // XXX: WORK AROUND due to a reconnection glitch. + // To remove when we have a proper way to refresh the delegation event ID without refreshing + // the whole credentials. + // We deliberately hide any changes to the SFU config because we + // do not want the app to reconnect whenever the JWT + // token changes due to us delegating a new delayed event. The + // initial SFU config for the transport is all the app needs. + distinctUntilChanged((prev, next) => + areLivekitTransportsEqual(prev.transport, next.transport), + ), + ), + null, + ), }; }; From 3503f8177d6885b2399fec92732efac7433557dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:31:03 +0000 Subject: [PATCH 30/33] Update dependency @vector-im/compound-design-tokens to v9 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d2c66504..5ff813b1 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@typescript-eslint/eslint-plugin": "^8.31.0", "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", - "@vector-im/compound-design-tokens": "^8.0.0", + "@vector-im/compound-design-tokens": "^9.0.0", "@vector-im/compound-web": "^8.0.0", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", diff --git a/yarn.lock b/yarn.lock index 23aa7b1f..ff8508cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6365,9 +6365,9 @@ __metadata: languageName: node linkType: hard -"@vector-im/compound-design-tokens@npm:^8.0.0": - version: 8.0.0 - resolution: "@vector-im/compound-design-tokens@npm:8.0.0" +"@vector-im/compound-design-tokens@npm:^9.0.0": + version: 9.0.0 + resolution: "@vector-im/compound-design-tokens@npm:9.0.0" peerDependencies: "@types/react": "*" react: ^17 || ^18 || ^19.0.0 @@ -6376,7 +6376,7 @@ __metadata: optional: true react: optional: true - checksum: 10c0/caa7a5ba9930b9f2eb8ed6282393263c91283d851924efa178db312b7ee221ed3e1ccc343954348ae52a7b11a3445ab9f92b150b89f0e525a6ff57672add016c + checksum: 10c0/6c53770bfba512d8a9f330ca2d0c481806e35f40d98f53815716e41ddac74d6fc3c4788fcda2e33907d62d2c5c04e64db62176c04513fbee41c7c436730081ce languageName: node linkType: hard @@ -8456,7 +8456,7 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^8.31.0" "@typescript-eslint/parser": "npm:^8.31.0" "@use-gesture/react": "npm:^10.2.11" - "@vector-im/compound-design-tokens": "npm:^8.0.0" + "@vector-im/compound-design-tokens": "npm:^9.0.0" "@vector-im/compound-web": "npm:^8.0.0" "@vitejs/plugin-react": "npm:^4.0.1" "@vitest/coverage-v8": "npm:^4.0.18" From 2c34e681a1232a4043e8a485802f87c065b9d3f5 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 6 Apr 2026 12:58:26 +0200 Subject: [PATCH 31/33] Fix tests --- .../localMember/LocalTransport.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 895f3073..cf7555fa 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -27,6 +27,7 @@ import { flushPromises, ownMemberMock, mockRtcMembership, + testScope, } from "../../../utils/test"; import { createLocalTransport$, @@ -51,13 +52,11 @@ describe("LocalTransport", () => { livekitIdentity: "@lk_user:ABCDEF", }; - let scope: ObservableScope; - beforeEach(() => (scope = new ObservableScope())); - afterEach(() => scope.end()); + beforeEach(() => vi.clearAllMocks()); it("throws if config is missing", async () => { const { advertised$, active$ } = createLocalTransport$({ - scope, + scope: testScope(), roomId: "!room:example.org", useOldestMember: false, memberships$: constant(new Epoch([])), @@ -144,7 +143,7 @@ describe("LocalTransport", () => { ); const { advertised$, active$ } = createLocalTransport$({ - scope, + scope: testScope(), roomId: "!room:example.org", useOldestMember: false, memberships$: constant(new Epoch([])), @@ -215,6 +214,7 @@ describe("LocalTransport", () => { // Initially, Alice is the only member const memberships$ = new BehaviorSubject([aliceMembership]); + const scope = testScope(); const { advertised$, active$ } = createLocalTransport$({ scope, roomId: "!example_room_id", @@ -271,6 +271,7 @@ describe("LocalTransport", () => { // Initially, there are no members const memberships$ = new BehaviorSubject([]); + const scope = testScope(); const { advertised$, active$ } = createLocalTransport$({ scope, roomId: "!example_room_id", @@ -321,7 +322,7 @@ describe("LocalTransport", () => { customLivekitUrl.setValue(customLivekitUrl.defaultValue); localTransportOpts = { ownMembershipIdentity: ownMemberMock, - scope, + scope: testScope(), roomId: "!example_room_id", useOldestMember: false, forceJwtEndpoint: JwtEndpointVersion.Legacy, @@ -515,7 +516,7 @@ describe("LocalTransport", () => { it("throws if no options are available", async () => { const { advertised$, active$ } = createLocalTransport$({ - scope, + scope: testScope(), ownMembershipIdentity: ownMemberMock, roomId: "!example_room_id", useOldestMember: false, @@ -555,7 +556,7 @@ describe("LocalTransport", () => { const delayId$ = new BehaviorSubject(null); const { advertised$, active$ } = createLocalTransport$({ - scope, + scope: testScope(), ownMembershipIdentity: ownMemberMock, roomId: "!example_room_id", // We want multi-sdu From 89c4485f2d0c1bc3cb9705c53aa0bc5c4fa71dfc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:16:47 +0000 Subject: [PATCH 32/33] Update dependency vite to v8 [SECURITY] --- package.json | 2 +- yarn.lock | 580 ++------------------------------------------------- 2 files changed, 24 insertions(+), 558 deletions(-) diff --git a/package.json b/package.json index 5ff813b1..ca1fe544 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "unique-names-generator": "^4.6.0", "uuid": "^13.0.0", "vaul": "^1.0.0", - "vite": "^7.3.0", + "vite": "^8.0.0", "vite-plugin-generate-file": "^0.3.0", "vite-plugin-html": "^3.2.2", "vite-plugin-node-stdlib-browser": "^0.2.1", diff --git a/yarn.lock b/yarn.lock index ff8508cf..14ee3117 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2518,13 +2518,6 @@ __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" @@ -2532,13 +2525,6 @@ __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" @@ -2546,13 +2532,6 @@ __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" @@ -2560,13 +2539,6 @@ __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" @@ -2574,13 +2546,6 @@ __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" @@ -2588,13 +2553,6 @@ __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" @@ -2602,13 +2560,6 @@ __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" @@ -2616,13 +2567,6 @@ __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" @@ -2630,13 +2574,6 @@ __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" @@ -2644,13 +2581,6 @@ __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" @@ -2658,13 +2588,6 @@ __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" @@ -2672,13 +2595,6 @@ __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" @@ -2686,13 +2602,6 @@ __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" @@ -2700,13 +2609,6 @@ __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" @@ -2714,13 +2616,6 @@ __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" @@ -2728,13 +2623,6 @@ __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" @@ -2742,13 +2630,6 @@ __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" @@ -2756,13 +2637,6 @@ __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" @@ -2770,13 +2644,6 @@ __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" @@ -2784,13 +2651,6 @@ __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" @@ -2798,20 +2658,6 @@ __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" @@ -2819,13 +2665,6 @@ __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" @@ -2833,13 +2672,6 @@ __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" @@ -2847,13 +2679,6 @@ __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" @@ -2861,13 +2686,6 @@ __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" @@ -5194,181 +5012,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@rollup/rollup-android-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-android-arm64@npm:4.59.0" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-darwin-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-darwin-x64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@rollup/rollup-freebsd-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-freebsd-x64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" - conditions: os=linux & cpu=arm & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" - conditions: os=linux & cpu=arm & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-arm64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" - conditions: os=linux & cpu=loong64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-loong64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" - conditions: os=linux & cpu=loong64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" - conditions: os=linux & cpu=ppc64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" - conditions: os=linux & cpu=ppc64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" - conditions: os=linux & cpu=riscv64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" - conditions: os=linux & cpu=riscv64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" - conditions: os=linux & cpu=s390x & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-x64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-x64-musl@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@rollup/rollup-openbsd-x64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"@rollup/rollup-openharmony-arm64@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@rollup/rollup-win32-x64-gnu@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@rollup/rollup-win32-x64-msvc@npm:4.59.0": - version: 4.59.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -5871,13 +5514,6 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.8": - version: 1.0.8 - resolution: "@types/estree@npm:1.0.8" - checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 - languageName: node - linkType: hard - "@types/estree@npm:^1.0.0": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -5885,6 +5521,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.8": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 + languageName: node + linkType: hard + "@types/events@npm:^3.0.0": version: 3.0.3 resolution: "@types/events@npm:3.0.3" @@ -8508,7 +8151,7 @@ __metadata: unique-names-generator: "npm:^4.6.0" uuid: "npm:^13.0.0" vaul: "npm:^1.0.0" - vite: "npm:^7.3.0" + vite: "npm:^8.0.0" vite-plugin-generate-file: "npm:^0.3.0" vite-plugin-html: "npm:^3.2.2" vite-plugin-node-stdlib-browser: "npm:^0.2.1" @@ -8928,95 +8571,6 @@ __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" @@ -13020,7 +12574,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.41, postcss@npm:^8.5.6, postcss@npm:^8.5.8": +"postcss@npm:^8.4.41, postcss@npm:^8.5.8": version: 8.5.8 resolution: "postcss@npm:8.5.8" dependencies: @@ -13901,96 +13455,6 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.43.0": - version: 4.59.0 - resolution: "rollup@npm:4.59.0" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.59.0" - "@rollup/rollup-android-arm64": "npm:4.59.0" - "@rollup/rollup-darwin-arm64": "npm:4.59.0" - "@rollup/rollup-darwin-x64": "npm:4.59.0" - "@rollup/rollup-freebsd-arm64": "npm:4.59.0" - "@rollup/rollup-freebsd-x64": "npm:4.59.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" - "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" - "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" - "@rollup/rollup-linux-x64-musl": "npm:4.59.0" - "@rollup/rollup-openbsd-x64": "npm:4.59.0" - "@rollup/rollup-openharmony-arm64": "npm:4.59.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" - "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" - "@types/estree": "npm:1.0.8" - fsevents: "npm:~2.3.2" - dependenciesMeta: - "@rollup/rollup-android-arm-eabi": - optional: true - "@rollup/rollup-android-arm64": - optional: true - "@rollup/rollup-darwin-arm64": - optional: true - "@rollup/rollup-darwin-x64": - optional: true - "@rollup/rollup-freebsd-arm64": - optional: true - "@rollup/rollup-freebsd-x64": - optional: true - "@rollup/rollup-linux-arm-gnueabihf": - optional: true - "@rollup/rollup-linux-arm-musleabihf": - optional: true - "@rollup/rollup-linux-arm64-gnu": - optional: true - "@rollup/rollup-linux-arm64-musl": - optional: true - "@rollup/rollup-linux-loong64-gnu": - optional: true - "@rollup/rollup-linux-loong64-musl": - optional: true - "@rollup/rollup-linux-ppc64-gnu": - optional: true - "@rollup/rollup-linux-ppc64-musl": - optional: true - "@rollup/rollup-linux-riscv64-gnu": - optional: true - "@rollup/rollup-linux-riscv64-musl": - optional: true - "@rollup/rollup-linux-s390x-gnu": - optional: true - "@rollup/rollup-linux-x64-gnu": - optional: true - "@rollup/rollup-linux-x64-musl": - optional: true - "@rollup/rollup-openbsd-x64": - optional: true - "@rollup/rollup-openharmony-arm64": - optional: true - "@rollup/rollup-win32-arm64-msvc": - optional: true - "@rollup/rollup-win32-ia32-msvc": - optional: true - "@rollup/rollup-win32-x64-gnu": - optional: true - "@rollup/rollup-win32-x64-msvc": - optional: true - fsevents: - optional: true - bin: - rollup: dist/bin/rollup - checksum: 10c0/f38742da34cfee5e899302615fa157aa77cb6a2a1495e5e3ce4cc9c540d3262e235bbe60caa31562bbfe492b01fdb3e7a8c43c39d842d3293bcf843123b766fc - languageName: node - linkType: hard - "rrweb-cssom@npm:^0.8.0": version: 0.8.0 resolution: "rrweb-cssom@npm:0.8.0" @@ -15724,22 +15188,22 @@ __metadata: languageName: node linkType: hard -"vite@npm:^7.3.0": - version: 7.3.1 - resolution: "vite@npm:7.3.1" +"vite@npm:^8.0.0": + version: 8.0.5 + resolution: "vite@npm:8.0.5" dependencies: - esbuild: "npm:^0.27.0" - fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" - picomatch: "npm:^4.0.3" - postcss: "npm:^8.5.6" - rollup: "npm:^4.43.0" + lightningcss: "npm:^1.32.0" + picomatch: "npm:^4.0.4" + postcss: "npm:^8.5.8" + rolldown: "npm:1.0.0-rc.12" tinyglobby: "npm:^0.2.15" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: ">=1.21.0" less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: ">=0.54.8" @@ -15753,12 +15217,14 @@ __metadata: peerDependenciesMeta: "@types/node": optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -15775,7 +15241,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/5c7548f5f43a23533e53324304db4ad85f1896b1bfd3ee32ae9b866bac2933782c77b350eb2b52a02c625c8ad1ddd4c000df077419410650c982cd97fde8d014 + checksum: 10c0/bfc22896b2661753c01c398a058f1859bdbd3ebe55f3d8505ab629b39e5f68790c0a6f55f8644b6692b0b9b8e210f698082ef9f4fd0d76509f4a46762fbfbba2 languageName: node linkType: hard From 31b35463fccacf9e34a6a9712f189601912837d0 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 7 Apr 2026 01:31:21 +0200 Subject: [PATCH 33/33] Fix build --- vite.config.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 97d643ec..84ce6d3e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -125,10 +125,6 @@ export default ({ // Default naming fallback return "assets/[name]-[hash][extname]"; }, - manualChunks: { - // we should be able to remove this one https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/167 lands - "matrix-sdk-crypto-wasm": ["@matrix-org/matrix-sdk-crypto-wasm"], - }, }, }, },