Support MSC4143 RTC Transport endpoint (#3629)
* Use rtc-focus branch of js-sdk * Update makeTransport to fetch backend transports and validate all transports before response. * Fix test * Add test * Loads more tests * Add tests for openid errors * improve comment * update to develop commit * Add JWT parsing * Use JWT * Cleanup * fixup tests * fixup tests * lint * lint lint * Fix `Reconnecting`
This commit is contained in:
@@ -111,7 +111,7 @@
|
|||||||
"livekit-client": "^2.13.0",
|
"livekit-client": "^2.13.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"loglevel": "^1.9.1",
|
"loglevel": "^1.9.1",
|
||||||
"matrix-js-sdk": "^39.2.0",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#2218ec4e3102e841ba3e794e1c492c0a5aa6c1c3",
|
||||||
"matrix-widget-api": "^1.14.0",
|
"matrix-widget-api": "^1.14.0",
|
||||||
"node-stdlib-browser": "^1.3.1",
|
"node-stdlib-browser": "^1.3.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { createJTWToken } from "./fixtures/jwt-token";
|
||||||
|
|
||||||
test("Should show error screen if fails to get JWT token", async ({ page }) => {
|
test("Should show error screen if fails to get JWT token", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ test("Should show error screen if call creation is restricted", async ({
|
|||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: "wss://badurltotricktest/livekit/sfu",
|
url: "wss://badurltotricktest/livekit/sfu",
|
||||||
jwt: "FAKE",
|
jwt: createJTWToken("@fake:user", "!fake:room"),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
22
playwright/fixtures/jwt-token.ts
Normal file
22
playwright/fixtures/jwt-token.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function createJTWToken(sub: string, room: string): string {
|
||||||
|
return [
|
||||||
|
{}, // header
|
||||||
|
{
|
||||||
|
// payload
|
||||||
|
sub,
|
||||||
|
video: {
|
||||||
|
room,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}, // signature
|
||||||
|
]
|
||||||
|
.map((d) => global.btoa(JSON.stringify(d)))
|
||||||
|
.join(".");
|
||||||
|
}
|
||||||
@@ -68,11 +68,6 @@ test("When creator left, avoid reconnect to the same SFU", async ({
|
|||||||
reducedMotion: "reduce",
|
reducedMotion: "reduce",
|
||||||
});
|
});
|
||||||
const guestCPage = await guestC.newPage();
|
const guestCPage = await guestC.newPage();
|
||||||
let sfuGetCallCount = 0;
|
|
||||||
await guestCPage.route("**/livekit/jwt/sfu/get", async (route) => {
|
|
||||||
sfuGetCallCount++;
|
|
||||||
await route.continue();
|
|
||||||
});
|
|
||||||
// Track WebSocket connections
|
// Track WebSocket connections
|
||||||
let wsConnectionCount = 0;
|
let wsConnectionCount = 0;
|
||||||
await guestCPage.routeWebSocket("**", (ws) => {
|
await guestCPage.routeWebSocket("**", (ws) => {
|
||||||
@@ -100,5 +95,4 @@ test("When creator left, avoid reconnect to the same SFU", async ({
|
|||||||
// https://github.com/element-hq/element-call/issues/3344
|
// https://github.com/element-hq/element-call/issues/3344
|
||||||
// The app used to request a new jwt token then to reconnect to the SFU
|
// The app used to request a new jwt token then to reconnect to the SFU
|
||||||
expect(wsConnectionCount).toBe(1);
|
expect(wsConnectionCount).toBe(1);
|
||||||
expect(sfuGetCallCount).toBe(2 /* the first one is for the warmup */);
|
|
||||||
});
|
});
|
||||||
|
|||||||
112
src/livekit/openIDSFU.test.ts
Normal file
112
src/livekit/openIDSFU.test.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
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,
|
||||||
|
afterEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
type MockedObject,
|
||||||
|
vitest,
|
||||||
|
} from "vitest";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
|
||||||
|
import { getSFUConfigWithOpenID, type OpenIDClientParts } from "./openIDSFU";
|
||||||
|
import { testJWTToken } from "../utils/test-fixtures";
|
||||||
|
|
||||||
|
const sfuUrl = "https://sfu.example.org";
|
||||||
|
|
||||||
|
describe("getSFUConfigWithOpenID", () => {
|
||||||
|
let matrixClient: MockedObject<OpenIDClientParts>;
|
||||||
|
beforeEach(() => {
|
||||||
|
matrixClient = {
|
||||||
|
getOpenIdToken: vitest.fn(),
|
||||||
|
getDeviceId: vitest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vitest.clearAllMocks();
|
||||||
|
fetchMock.reset();
|
||||||
|
});
|
||||||
|
it("should handle fetching a token", async () => {
|
||||||
|
fetchMock.post("https://sfu.example.org/sfu/get", () => {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { url: sfuUrl, jwt: testJWTToken },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const config = await getSFUConfigWithOpenID(
|
||||||
|
matrixClient,
|
||||||
|
"https://sfu.example.org",
|
||||||
|
"!example_room_id",
|
||||||
|
);
|
||||||
|
expect(config).toEqual({
|
||||||
|
jwt: testJWTToken,
|
||||||
|
url: sfuUrl,
|
||||||
|
livekitIdentity: "@me:example.org:ABCDEF",
|
||||||
|
livekitAlias: "!example_room_id",
|
||||||
|
});
|
||||||
|
void (await fetchMock.flush());
|
||||||
|
});
|
||||||
|
it("should fail if the SFU errors", async () => {
|
||||||
|
fetchMock.post("https://sfu.example.org/sfu/get", () => {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: { error: "Test failure" },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await getSFUConfigWithOpenID(
|
||||||
|
matrixClient,
|
||||||
|
"https://sfu.example.org",
|
||||||
|
"!example_room_id",
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
expect(((ex as Error).cause as Error).message).toEqual(
|
||||||
|
"SFU Config fetch failed with status code 500",
|
||||||
|
);
|
||||||
|
void (await fetchMock.flush());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("Expected test to throw;");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should retry fetching the openid token", async () => {
|
||||||
|
let count = 0;
|
||||||
|
matrixClient.getOpenIdToken.mockImplementation(async () => {
|
||||||
|
count++;
|
||||||
|
if (count < 2) {
|
||||||
|
throw Error("Test failure");
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
token_type: "Bearer",
|
||||||
|
access_token: "foobar",
|
||||||
|
matrix_server_name: "example.org",
|
||||||
|
expires_in: 30,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fetchMock.post("https://sfu.example.org/sfu/get", () => {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { url: sfuUrl, jwt: testJWTToken },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const config = await getSFUConfigWithOpenID(
|
||||||
|
matrixClient,
|
||||||
|
"https://sfu.example.org",
|
||||||
|
"!example_room_id",
|
||||||
|
);
|
||||||
|
expect(config).toEqual({
|
||||||
|
jwt: testJWTToken,
|
||||||
|
url: sfuUrl,
|
||||||
|
livekitIdentity: "@me:example.org:ABCDEF",
|
||||||
|
livekitAlias: "!example_room_id",
|
||||||
|
});
|
||||||
|
void (await fetchMock.flush());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,9 +11,47 @@ import { logger } from "matrix-js-sdk/lib/logger";
|
|||||||
import { FailToGetOpenIdToken } from "../utils/errors";
|
import { FailToGetOpenIdToken } from "../utils/errors";
|
||||||
import { doNetworkOperationWithRetry } from "../utils/matrix";
|
import { doNetworkOperationWithRetry } from "../utils/matrix";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration and access tokens provided by the SFU on successful authentication.
|
||||||
|
*/
|
||||||
export interface SFUConfig {
|
export interface SFUConfig {
|
||||||
url: string;
|
url: string;
|
||||||
jwt: string;
|
jwt: string;
|
||||||
|
livekitAlias: string;
|
||||||
|
livekitIdentity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decoded details from the JWT.
|
||||||
|
*/
|
||||||
|
interface SFUJWTPayload {
|
||||||
|
/**
|
||||||
|
* Expiration time for the JWT.
|
||||||
|
* Note: This value is in seconds since Unix epoch.
|
||||||
|
*/
|
||||||
|
exp: number;
|
||||||
|
/**
|
||||||
|
* Name of the instance which authored the JWT
|
||||||
|
*/
|
||||||
|
iss: string;
|
||||||
|
/**
|
||||||
|
* Time at which the JWT can start to be used.
|
||||||
|
* Note: This value is in seconds since Unix epoch.
|
||||||
|
*/
|
||||||
|
nbf: number;
|
||||||
|
/**
|
||||||
|
* Subject. The Livekit alias in this context.
|
||||||
|
*/
|
||||||
|
sub: string;
|
||||||
|
/**
|
||||||
|
* The set of permissions for the user.
|
||||||
|
*/
|
||||||
|
video: {
|
||||||
|
canPublish: boolean;
|
||||||
|
canSubscribe: boolean;
|
||||||
|
room: string;
|
||||||
|
roomJoin: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bits we need from MatrixClient
|
// The bits we need from MatrixClient
|
||||||
@@ -57,7 +95,17 @@ export async function getSFUConfigWithOpenID(
|
|||||||
);
|
);
|
||||||
logger.info(`Got JWT from call's active focus URL.`);
|
logger.info(`Got JWT from call's active focus URL.`);
|
||||||
|
|
||||||
return sfuConfig;
|
// Pull the details from the JWT
|
||||||
|
const [, payloadStr] = sfuConfig.jwt.split(".");
|
||||||
|
// TODO: Prefer Uint8Array.fromBase64 when widely available
|
||||||
|
const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload;
|
||||||
|
return {
|
||||||
|
jwt: sfuConfig.jwt,
|
||||||
|
url: sfuConfig.url,
|
||||||
|
livekitAlias: payload.video.room,
|
||||||
|
// NOTE: Currently unused.
|
||||||
|
livekitIdentity: payload.sub,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLiveKitJWT(
|
async function getLiveKitJWT(
|
||||||
@@ -65,7 +113,7 @@ async function getLiveKitJWT(
|
|||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
roomName: string,
|
roomName: string,
|
||||||
openIDToken: IOpenIDToken,
|
openIDToken: IOpenIDToken,
|
||||||
): Promise<SFUConfig> {
|
): Promise<{ url: string; jwt: string }> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(livekitServiceURL + "/sfu/get", {
|
const res = await fetch(livekitServiceURL + "/sfu/get", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -83,6 +131,6 @@ async function getLiveKitJWT(
|
|||||||
}
|
}
|
||||||
return await res.json();
|
return await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error("SFU Config fetch failed with exception " + e);
|
throw new Error("SFU Config fetch failed with exception", { cause: e });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1256,7 +1256,9 @@ describe.each([
|
|||||||
rtcSession.membershipStatus = Status.Connected;
|
rtcSession.membershipStatus = Status.Connected;
|
||||||
},
|
},
|
||||||
n: () => {
|
n: () => {
|
||||||
rtcSession.membershipStatus = Status.Reconnecting;
|
// NOTE: This was removed in https://github.com/matrix-org/matrix-js-sdk/pull/5103 accidentally.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
rtcSession.membershipStatus = "Reconnecting" as any;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
schedule(probablyLeftMarbles, {
|
schedule(probablyLeftMarbles, {
|
||||||
|
|||||||
@@ -5,9 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
type MockedObject,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, lastValueFrom } from "rxjs";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
|
||||||
import { mockConfig, flushPromises } from "../../../utils/test";
|
import { mockConfig, flushPromises } from "../../../utils/test";
|
||||||
import { createLocalTransport$ } from "./LocalTransport";
|
import { createLocalTransport$ } from "./LocalTransport";
|
||||||
@@ -18,32 +27,23 @@ import {
|
|||||||
FailToGetOpenIdToken,
|
FailToGetOpenIdToken,
|
||||||
} from "../../../utils/errors";
|
} from "../../../utils/errors";
|
||||||
import * as openIDSFU from "../../../livekit/openIDSFU";
|
import * as openIDSFU from "../../../livekit/openIDSFU";
|
||||||
|
import { customLivekitUrl } from "../../../settings/settings";
|
||||||
|
import { testJWTToken } from "../../../utils/test-fixtures";
|
||||||
|
|
||||||
describe("LocalTransport", () => {
|
describe("LocalTransport", () => {
|
||||||
|
const openIdResponse: openIDSFU.SFUConfig = {
|
||||||
|
url: "https://lk.example.org",
|
||||||
|
jwt: testJWTToken,
|
||||||
|
livekitAlias: "!example_room_id",
|
||||||
|
livekitIdentity: "@lk_user:ABCDEF",
|
||||||
|
};
|
||||||
|
|
||||||
let scope: ObservableScope;
|
let scope: ObservableScope;
|
||||||
beforeEach(() => (scope = new ObservableScope()));
|
beforeEach(() => {
|
||||||
|
scope = new ObservableScope();
|
||||||
|
});
|
||||||
afterEach(() => scope.end());
|
afterEach(() => scope.end());
|
||||||
|
|
||||||
it("throws if config is missing", async () => {
|
|
||||||
const localTransport$ = createLocalTransport$({
|
|
||||||
scope,
|
|
||||||
roomId: "!room:example.org",
|
|
||||||
useOldestMember$: constant(false),
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
|
||||||
client: {
|
|
||||||
getDomain: () => "",
|
|
||||||
// These won't be called in this error path but satisfy the type
|
|
||||||
getOpenIdToken: vi.fn(),
|
|
||||||
getDeviceId: vi.fn(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
expect(() => localTransport$.value).toThrow(
|
|
||||||
new MatrixRTCTransportMissingError(""),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws FailToGetOpenIdToken when OpenID fetch fails", async () => {
|
it("throws FailToGetOpenIdToken when OpenID fetch fails", async () => {
|
||||||
// Provide a valid config so makeTransportInternal resolves a transport
|
// Provide a valid config so makeTransportInternal resolves a transport
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
@@ -61,12 +61,14 @@ describe("LocalTransport", () => {
|
|||||||
const errors: Error[] = [];
|
const errors: Error[] = [];
|
||||||
const localTransport$ = createLocalTransport$({
|
const localTransport$ = createLocalTransport$({
|
||||||
scope,
|
scope,
|
||||||
roomId: "!room:example.org",
|
roomId: "!example_room_id",
|
||||||
useOldestMember$: constant(false),
|
useOldestMember$: constant(false),
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
client: {
|
client: {
|
||||||
// Use empty domain to skip .well-known and use config directly
|
// Use empty domain to skip .well-known and use config directly
|
||||||
getDomain: () => "",
|
getDomain: () => "",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||||
getOpenIdToken: vi.fn(),
|
getOpenIdToken: vi.fn(),
|
||||||
getDeviceId: vi.fn(),
|
getDeviceId: vi.fn(),
|
||||||
},
|
},
|
||||||
@@ -84,41 +86,6 @@ describe("LocalTransport", () => {
|
|||||||
expect(() => localTransport$.value).toThrow(expectedError);
|
expect(() => localTransport$.value).toThrow(expectedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits preferred transport after OpenID resolves", async () => {
|
|
||||||
// Use config so transport discovery succeeds, but delay OpenID JWT fetch
|
|
||||||
mockConfig({
|
|
||||||
livekit: { livekit_service_url: "https://lk.example.org" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const openIdResolver = Promise.withResolvers<openIDSFU.SFUConfig>();
|
|
||||||
|
|
||||||
vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockReturnValue(
|
|
||||||
openIdResolver.promise,
|
|
||||||
);
|
|
||||||
|
|
||||||
const localTransport$ = createLocalTransport$({
|
|
||||||
scope,
|
|
||||||
roomId: "!room:example.org",
|
|
||||||
useOldestMember$: constant(false),
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
|
||||||
client: {
|
|
||||||
getDomain: () => "",
|
|
||||||
getOpenIdToken: vi.fn(),
|
|
||||||
getDeviceId: vi.fn(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
openIdResolver.resolve?.({ url: "https://lk.example.org", jwt: "jwt" });
|
|
||||||
expect(localTransport$.value).toBe(null);
|
|
||||||
await flushPromises();
|
|
||||||
// final
|
|
||||||
expect(localTransport$.value).toStrictEqual({
|
|
||||||
livekit_alias: "!room:example.org",
|
|
||||||
livekit_service_url: "https://lk.example.org",
|
|
||||||
type: "livekit",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates local transport when oldest member changes", async () => {
|
it("updates local transport when oldest member changes", async () => {
|
||||||
// Use config so transport discovery succeeds, but delay OpenID JWT fetch
|
// Use config so transport discovery succeeds, but delay OpenID JWT fetch
|
||||||
mockConfig({
|
mockConfig({
|
||||||
@@ -133,24 +100,171 @@ describe("LocalTransport", () => {
|
|||||||
|
|
||||||
const localTransport$ = createLocalTransport$({
|
const localTransport$ = createLocalTransport$({
|
||||||
scope,
|
scope,
|
||||||
roomId: "!room:example.org",
|
roomId: "!example_room_id",
|
||||||
useOldestMember$: constant(true),
|
useOldestMember$: constant(true),
|
||||||
memberships$,
|
memberships$,
|
||||||
client: {
|
client: {
|
||||||
getDomain: () => "",
|
getDomain: () => "",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||||
getOpenIdToken: vi.fn(),
|
getOpenIdToken: vi.fn(),
|
||||||
getDeviceId: vi.fn(),
|
getDeviceId: vi.fn(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
openIdResolver.resolve?.({ url: "https://lk.example.org", jwt: "jwt" });
|
openIdResolver.resolve?.(openIdResponse);
|
||||||
expect(localTransport$.value).toBe(null);
|
expect(localTransport$.value).toBe(null);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
// final
|
// final
|
||||||
expect(localTransport$.value).toStrictEqual({
|
expect(localTransport$.value).toStrictEqual({
|
||||||
livekit_alias: "!room:example.org",
|
livekit_alias: "!example_room_id",
|
||||||
livekit_service_url: "https://lk.example.org",
|
livekit_service_url: "https://lk.example.org",
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type LocalTransportProps = Parameters<typeof createLocalTransport$>[0];
|
||||||
|
|
||||||
|
describe("transport configuration mechanisms", () => {
|
||||||
|
let localTransportOpts: LocalTransportProps & {
|
||||||
|
client: MockedObject<LocalTransportProps["client"]>;
|
||||||
|
};
|
||||||
|
let openIdResolver: PromiseWithResolvers<openIDSFU.SFUConfig>;
|
||||||
|
beforeEach(() => {
|
||||||
|
mockConfig({});
|
||||||
|
customLivekitUrl.setValue(customLivekitUrl.defaultValue);
|
||||||
|
localTransportOpts = {
|
||||||
|
scope,
|
||||||
|
roomId: "!example_room_id",
|
||||||
|
useOldestMember$: constant(false),
|
||||||
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
|
client: {
|
||||||
|
getDomain: vi.fn().mockReturnValue(""),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
_unstable_getRTCTransports: vi.fn().mockResolvedValue([]),
|
||||||
|
getOpenIdToken: vi.fn(),
|
||||||
|
getDeviceId: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
openIdResolver = Promise.withResolvers<openIDSFU.SFUConfig>();
|
||||||
|
vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockReturnValue(
|
||||||
|
openIdResolver.promise,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports getting transport via application config", async () => {
|
||||||
|
mockConfig({
|
||||||
|
livekit: { livekit_service_url: "https://lk.example.org" },
|
||||||
|
});
|
||||||
|
const localTransport$ = createLocalTransport$(localTransportOpts);
|
||||||
|
openIdResolver.resolve?.(openIdResponse);
|
||||||
|
expect(localTransport$.value).toBe(null);
|
||||||
|
await flushPromises();
|
||||||
|
expect(localTransport$.value).toStrictEqual({
|
||||||
|
livekit_alias: "!example_room_id",
|
||||||
|
livekit_service_url: "https://lk.example.org",
|
||||||
|
type: "livekit",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("supports getting transport via user settings", async () => {
|
||||||
|
customLivekitUrl.setValue("https://lk.example.org");
|
||||||
|
const localTransport$ = createLocalTransport$(localTransportOpts);
|
||||||
|
openIdResolver.resolve?.(openIdResponse);
|
||||||
|
expect(localTransport$.value).toBe(null);
|
||||||
|
await flushPromises();
|
||||||
|
expect(localTransport$.value).toStrictEqual({
|
||||||
|
livekit_alias: "!example_room_id",
|
||||||
|
livekit_service_url: "https://lk.example.org",
|
||||||
|
type: "livekit",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("supports getting transport via backend", async () => {
|
||||||
|
localTransportOpts.client._unstable_getRTCTransports.mockResolvedValue([
|
||||||
|
{ type: "livekit", livekit_service_url: "https://lk.example.org" },
|
||||||
|
]);
|
||||||
|
const localTransport$ = createLocalTransport$(localTransportOpts);
|
||||||
|
openIdResolver.resolve?.(openIdResponse);
|
||||||
|
expect(localTransport$.value).toBe(null);
|
||||||
|
await flushPromises();
|
||||||
|
expect(localTransport$.value).toStrictEqual({
|
||||||
|
livekit_alias: "!example_room_id",
|
||||||
|
livekit_service_url: "https://lk.example.org",
|
||||||
|
type: "livekit",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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" },
|
||||||
|
]);
|
||||||
|
openIdResolver.reject(
|
||||||
|
new FailToGetOpenIdToken(new Error("Test driven error")),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await lastValueFrom(createLocalTransport$(localTransportOpts));
|
||||||
|
throw Error("Expected test to throw");
|
||||||
|
} catch (ex) {
|
||||||
|
expect(ex).toBeInstanceOf(FailToGetOpenIdToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("supports getting transport via well-known", async () => {
|
||||||
|
localTransportOpts.client.getDomain.mockReturnValue("example.org");
|
||||||
|
fetchMock.getOnce("https://example.org/.well-known/matrix/client", {
|
||||||
|
"org.matrix.msc4143.rtc_foci": [
|
||||||
|
{ type: "livekit", livekit_service_url: "https://lk.example.org" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const localTransport$ = createLocalTransport$(localTransportOpts);
|
||||||
|
openIdResolver.resolve?.(openIdResponse);
|
||||||
|
expect(localTransport$.value).toBe(null);
|
||||||
|
await flushPromises();
|
||||||
|
expect(localTransport$.value).toStrictEqual({
|
||||||
|
livekit_alias: "!example_room_id",
|
||||||
|
livekit_service_url: "https://lk.example.org",
|
||||||
|
type: "livekit",
|
||||||
|
});
|
||||||
|
expect(fetchMock.done()).toEqual(true);
|
||||||
|
});
|
||||||
|
it("fails fast if the openId request fails for the well-known config", async () => {
|
||||||
|
localTransportOpts.client.getDomain.mockReturnValue("example.org");
|
||||||
|
fetchMock.getOnce("https://example.org/.well-known/matrix/client", {
|
||||||
|
"org.matrix.msc4143.rtc_foci": [
|
||||||
|
{ type: "livekit", livekit_service_url: "https://lk.example.org" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
openIdResolver.reject(
|
||||||
|
new FailToGetOpenIdToken(new Error("Test driven error")),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await lastValueFrom(createLocalTransport$(localTransportOpts));
|
||||||
|
throw Error("Expected test to throw");
|
||||||
|
} catch (ex) {
|
||||||
|
expect(ex).toBeInstanceOf(FailToGetOpenIdToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("throws if no options are available", async () => {
|
||||||
|
const localTransport$ = createLocalTransport$({
|
||||||
|
scope,
|
||||||
|
roomId: "!example_room_id",
|
||||||
|
useOldestMember$: constant(false),
|
||||||
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
|
client: {
|
||||||
|
getDomain: () => "",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||||
|
// These won't be called in this error path but satisfy the type
|
||||||
|
getOpenIdToken: vi.fn(),
|
||||||
|
getDeviceId: vi.fn(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(() => localTransport$.value).toThrow(
|
||||||
|
new MatrixRTCTransportMissingError(""),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
isLivekitTransport,
|
isLivekitTransport,
|
||||||
type LivekitTransportConfig,
|
|
||||||
type LivekitTransport,
|
type LivekitTransport,
|
||||||
isLivekitTransportConfig,
|
isLivekitTransportConfig,
|
||||||
|
type Transport,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { MatrixError, type MatrixClient } from "matrix-js-sdk";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
@@ -27,7 +27,10 @@ import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
|||||||
import { type Behavior } from "../../Behavior.ts";
|
import { type Behavior } from "../../Behavior.ts";
|
||||||
import { type Epoch, type ObservableScope } from "../../ObservableScope.ts";
|
import { type Epoch, type ObservableScope } from "../../ObservableScope.ts";
|
||||||
import { Config } from "../../../config/Config.ts";
|
import { Config } from "../../../config/Config.ts";
|
||||||
import { MatrixRTCTransportMissingError } from "../../../utils/errors.ts";
|
import {
|
||||||
|
FailToGetOpenIdToken,
|
||||||
|
MatrixRTCTransportMissingError,
|
||||||
|
} from "../../../utils/errors.ts";
|
||||||
import {
|
import {
|
||||||
getSFUConfigWithOpenID,
|
getSFUConfigWithOpenID,
|
||||||
type OpenIDClientParts,
|
type OpenIDClientParts,
|
||||||
@@ -45,7 +48,8 @@ const logger = rootLogger.getChild("[LocalTransport]");
|
|||||||
interface Props {
|
interface Props {
|
||||||
scope: ObservableScope;
|
scope: ObservableScope;
|
||||||
memberships$: Behavior<Epoch<CallMembership[]>>;
|
memberships$: Behavior<Epoch<CallMembership[]>>;
|
||||||
client: Pick<MatrixClient, "getDomain"> & OpenIDClientParts;
|
client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
|
||||||
|
OpenIDClientParts;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
useOldestMember$: Behavior<boolean>;
|
useOldestMember$: Behavior<boolean>;
|
||||||
}
|
}
|
||||||
@@ -116,73 +120,150 @@ export const createLocalTransport$ = ({
|
|||||||
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
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:
|
||||||
*
|
*
|
||||||
* @param client
|
* 1. The `urlFromDevSettings` value. If this cannot be validated, the function will throw.
|
||||||
* @param roomId
|
* 2. The transports returned via the homeserver.
|
||||||
* @returns
|
* 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 roomId The ID of the room to be connected to.
|
||||||
|
* @param urlFromDevSettings Override URL provided by the user's local config.
|
||||||
|
* @returns A fully validated transport config.
|
||||||
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
|
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
|
||||||
*/
|
*/
|
||||||
async function makeTransport(
|
async function makeTransport(
|
||||||
client: Pick<MatrixClient, "getDomain"> & OpenIDClientParts,
|
client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
|
||||||
|
OpenIDClientParts,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
urlFromDevSettings: string | null,
|
urlFromDevSettings: string | null,
|
||||||
): Promise<LivekitTransport> {
|
): Promise<LivekitTransport> {
|
||||||
let transport: LivekitTransport | undefined;
|
|
||||||
logger.trace("Searching for a preferred transport");
|
logger.trace("Searching for a preferred transport");
|
||||||
//TODO refactor this to use the jwt service returned alias.
|
|
||||||
const livekitAlias = roomId;
|
// 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
|
// DEVTOOL: Highest priority: Load from devtool setting
|
||||||
if (urlFromDevSettings !== null) {
|
if (urlFromDevSettings !== null) {
|
||||||
const transportFromStorage: LivekitTransport = {
|
logger.info("Using LiveKit transport from dev tools: ", urlFromDevSettings);
|
||||||
|
// Validate that the SFU is up. Otherwise, we want to fail on this
|
||||||
|
// as we don't permit other SFUs.
|
||||||
|
const config = await getSFUConfigWithOpenID(
|
||||||
|
client,
|
||||||
|
urlFromDevSettings,
|
||||||
|
roomId,
|
||||||
|
);
|
||||||
|
return {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: urlFromDevSettings,
|
livekit_service_url: urlFromDevSettings,
|
||||||
livekit_alias: livekitAlias,
|
livekit_alias: config.livekitAlias,
|
||||||
};
|
};
|
||||||
logger.info(
|
|
||||||
"Using LiveKit transport from dev tools: ",
|
|
||||||
transportFromStorage,
|
|
||||||
);
|
|
||||||
transport = transportFromStorage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WELL_KNOWN: Prioritize the .well-known/matrix/client, if available, over the configured SFU
|
async function getFirstUsableTransport(
|
||||||
|
transports: Transport[],
|
||||||
|
): Promise<LivekitTransport | null> {
|
||||||
|
for (const potentialTransport of transports) {
|
||||||
|
if (isLivekitTransportConfig(potentialTransport)) {
|
||||||
|
try {
|
||||||
|
const { livekitAlias } = await getSFUConfigWithOpenID(
|
||||||
|
client,
|
||||||
|
potentialTransport.livekit_service_url,
|
||||||
|
roomId,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...potentialTransport,
|
||||||
|
livekit_alias: livekitAlias,
|
||||||
|
};
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof FailToGetOpenIdToken) {
|
||||||
|
// Explictly throw these
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
logger.debug(
|
||||||
|
`Could not use SFU service "${potentialTransport.livekit_service_url}" as SFU`,
|
||||||
|
ex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSC4143: Attempt to fetch transports from backend.
|
||||||
|
if ("_unstable_getRTCTransports" in client) {
|
||||||
|
try {
|
||||||
|
const selectedTransport = await getFirstUsableTransport(
|
||||||
|
await client._unstable_getRTCTransports(),
|
||||||
|
);
|
||||||
|
if (selectedTransport) {
|
||||||
|
logger.info("Using backend-configured SFU", selectedTransport);
|
||||||
|
return selectedTransport;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof MatrixError && ex.httpStatus === 404) {
|
||||||
|
// Expected, this is an unstable endpoint and it's not required.
|
||||||
|
logger.debug("Backend does not provide any RTC transports", ex);
|
||||||
|
} 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy MSC4143 (to be removed) WELL_KNOWN: Prioritize the .well-known/matrix/client, if available.
|
||||||
const domain = client.getDomain();
|
const domain = client.getDomain();
|
||||||
if (domain && transport === undefined) {
|
if (domain) {
|
||||||
// we use AutoDiscovery instead of relying on the MatrixClient having already
|
// we use AutoDiscovery instead of relying on the MatrixClient having already
|
||||||
// been fully configured and started
|
// been fully configured and started
|
||||||
const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[
|
const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[
|
||||||
FOCI_WK_KEY
|
FOCI_WK_KEY
|
||||||
];
|
];
|
||||||
if (Array.isArray(wellKnownFoci)) {
|
const selectedTransport = Array.isArray(wellKnownFoci)
|
||||||
const wellKnownTransport: LivekitTransportConfig | undefined =
|
? await getFirstUsableTransport(wellKnownFoci)
|
||||||
wellKnownFoci.find((f) => f && isLivekitTransportConfig(f));
|
: null;
|
||||||
if (wellKnownTransport !== undefined) {
|
if (selectedTransport) {
|
||||||
logger.info("Using LiveKit transport from .well-known: ", transport);
|
logger.info("Using .well-known SFU", selectedTransport);
|
||||||
transport = { ...wellKnownTransport, livekit_alias: livekitAlias };
|
return selectedTransport;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONFIG: Least prioritized; Load from config file
|
// CONFIG: Least prioritized; Load from config file
|
||||||
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
||||||
if (urlFromConf && transport === undefined) {
|
if (urlFromConf) {
|
||||||
const transportFromConf: LivekitTransport = {
|
try {
|
||||||
|
const { livekitAlias } = await getSFUConfigWithOpenID(
|
||||||
|
client,
|
||||||
|
urlFromConf,
|
||||||
|
roomId,
|
||||||
|
);
|
||||||
|
const selectedTransport: LivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: urlFromConf,
|
livekit_service_url: urlFromConf,
|
||||||
livekit_alias: livekitAlias,
|
livekit_alias: livekitAlias,
|
||||||
};
|
};
|
||||||
logger.info("Using LiveKit transport from config: ", transportFromConf);
|
logger.info("Using config SFU", selectedTransport);
|
||||||
transport = transportFromConf;
|
return selectedTransport;
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof FailToGetOpenIdToken) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
logger.error("Failed to validate config SFU", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transport) throw new MatrixRTCTransportMissingError(domain ?? ""); // this will call the jwt/sfu/get endpoint to pre create the livekit room.
|
throw new MatrixRTCTransportMissingError(domain ?? "");
|
||||||
|
|
||||||
await getSFUConfigWithOpenID(
|
|
||||||
client,
|
|
||||||
transport.livekit_service_url,
|
|
||||||
transport.livekit_alias,
|
|
||||||
);
|
|
||||||
|
|
||||||
return transport;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
ElementCallError,
|
ElementCallError,
|
||||||
FailToGetOpenIdToken,
|
FailToGetOpenIdToken,
|
||||||
} from "../../../utils/errors.ts";
|
} from "../../../utils/errors.ts";
|
||||||
|
import { testJWTToken } from "../../../utils/test-fixtures.ts";
|
||||||
import { mockRemoteParticipant } from "../../../utils/test.ts";
|
import { mockRemoteParticipant } from "../../../utils/test.ts";
|
||||||
|
|
||||||
let testScope: ObservableScope;
|
let testScope: ObservableScope;
|
||||||
@@ -121,7 +122,7 @@ function setupRemoteConnection(): Connection {
|
|||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
|
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
|
||||||
jwt: "ATOKEN",
|
jwt: testJWTToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -258,7 +259,7 @@ describe("Start connection states", () => {
|
|||||||
capturedState.cause instanceof Error
|
capturedState.cause instanceof Error
|
||||||
) {
|
) {
|
||||||
expect(capturedState.cause.message).toContain(
|
expect(capturedState.cause.message).toContain(
|
||||||
"SFU Config fetch failed with exception Error",
|
"SFU Config fetch failed with exception",
|
||||||
);
|
);
|
||||||
expect(connection.transport.livekit_alias).toEqual(
|
expect(connection.transport.livekit_alias).toEqual(
|
||||||
livekitFocus.livekit_alias,
|
livekitFocus.livekit_alias,
|
||||||
@@ -294,7 +295,7 @@ describe("Start connection states", () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
|
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
|
||||||
jwt: "ATOKEN",
|
jwt: testJWTToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "./MatrixLivekitMembers.ts";
|
} from "./MatrixLivekitMembers.ts";
|
||||||
import { createConnectionManager$ } from "./ConnectionManager.ts";
|
import { createConnectionManager$ } from "./ConnectionManager.ts";
|
||||||
import { membershipsAndTransports$ } from "../../SessionBehaviors.ts";
|
import { membershipsAndTransports$ } from "../../SessionBehaviors.ts";
|
||||||
|
import { testJWTToken } from "../../../utils/test-fixtures.ts";
|
||||||
|
|
||||||
// Test the integration of ConnectionManager and MatrixLivekitMerger
|
// Test the integration of ConnectionManager and MatrixLivekitMerger
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ beforeEach(() => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
url: `wss://${domain}/livekit/sfu`,
|
url: `wss://${domain}/livekit/sfu`,
|
||||||
jwt: "ATOKEN",
|
jwt: testJWTToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,3 +59,17 @@ export const daveRTLRtcMember = mockRtcMembership("@dave2:example.org", "DDDD");
|
|||||||
export const daveRTL = mockMatrixRoomMember(daveRTLRtcMember, {
|
export const daveRTL = mockMatrixRoomMember(daveRTLRtcMember, {
|
||||||
rawDisplayName: "\u202eevaD",
|
rawDisplayName: "\u202eevaD",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const testJWTToken = [
|
||||||
|
{}, // header
|
||||||
|
{
|
||||||
|
// payload
|
||||||
|
sub: "@me:example.org:ABCDEF",
|
||||||
|
video: {
|
||||||
|
room: "!example_room_id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}, // signature
|
||||||
|
]
|
||||||
|
.map((d) => global.btoa(JSON.stringify(d)))
|
||||||
|
.join(".");
|
||||||
|
|||||||
18
yarn.lock
18
yarn.lock
@@ -2802,10 +2802,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm@npm:^15.3.0":
|
"@matrix-org/matrix-sdk-crypto-wasm@npm:^16.0.0":
|
||||||
version: 15.3.0
|
version: 16.0.0
|
||||||
resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:15.3.0"
|
resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:16.0.0"
|
||||||
checksum: 10c0/45628f36b7b0e54a8777ae67a7233dbdf3e3cf14e0d95d21f62f89a7ea7e3f907232f1eb7b1262193b1e227759fad47af829dcccc103ded89011f13c66f01d76
|
checksum: 10c0/13b4ede3e618da819957abff778afefcf3baf9a2faac04a36bb5a07a44fae2ea05fbfa072eb3408d48b2b7b9aaf27242ce52c594c8ce9bf1fb8b3aade2832be1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -7841,7 +7841,7 @@ __metadata:
|
|||||||
livekit-client: "npm:^2.13.0"
|
livekit-client: "npm:^2.13.0"
|
||||||
lodash-es: "npm:^4.17.21"
|
lodash-es: "npm:^4.17.21"
|
||||||
loglevel: "npm:^1.9.1"
|
loglevel: "npm:^1.9.1"
|
||||||
matrix-js-sdk: "npm:^39.2.0"
|
matrix-js-sdk: "matrix-org/matrix-js-sdk#2218ec4e3102e841ba3e794e1c492c0a5aa6c1c3"
|
||||||
matrix-widget-api: "npm:^1.14.0"
|
matrix-widget-api: "npm:^1.14.0"
|
||||||
node-stdlib-browser: "npm:^1.3.1"
|
node-stdlib-browser: "npm:^1.3.1"
|
||||||
normalize.css: "npm:^8.0.1"
|
normalize.css: "npm:^8.0.1"
|
||||||
@@ -10780,12 +10780,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"matrix-js-sdk@npm:^39.2.0":
|
"matrix-js-sdk@matrix-org/matrix-js-sdk#2218ec4e3102e841ba3e794e1c492c0a5aa6c1c3":
|
||||||
version: 39.3.0
|
version: 39.3.0
|
||||||
resolution: "matrix-js-sdk@npm:39.3.0"
|
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=2218ec4e3102e841ba3e794e1c492c0a5aa6c1c3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime": "npm:^7.12.5"
|
"@babel/runtime": "npm:^7.12.5"
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.3.0"
|
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0"
|
||||||
another-json: "npm:^0.2.0"
|
another-json: "npm:^0.2.0"
|
||||||
bs58: "npm:^6.0.0"
|
bs58: "npm:^6.0.0"
|
||||||
content-type: "npm:^1.0.4"
|
content-type: "npm:^1.0.4"
|
||||||
@@ -10798,7 +10798,7 @@ __metadata:
|
|||||||
sdp-transform: "npm:^3.0.0"
|
sdp-transform: "npm:^3.0.0"
|
||||||
unhomoglyph: "npm:^1.0.6"
|
unhomoglyph: "npm:^1.0.6"
|
||||||
uuid: "npm:13"
|
uuid: "npm:13"
|
||||||
checksum: 10c0/031c9ec042e00c32dc531f82fc59c64cc25fb665abfc642b1f0765c530d60684f8bd63daf0cdd0dbe96b4f87ea3f4148f9d3f024a59d57eceaec1ce5d0164755
|
checksum: 10c0/feca51c7ada5a56aa6cfb74f29bd1640a20804e9de689d23f10c5227e07ba4f66ebbb9606e1384390dca277a6942886706198394717694a9cfb1f20cd36ca377
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user