From 13df897896122fa3c68f0859fe34218962ea0131 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 13:42:27 +0000 Subject: [PATCH 01/55] v34.13.0-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef7342cd70e..6ebe111f056 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "34.12.0", + "version": "34.13.0-rc.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=20.0.0" From 125e45c24db9d0d5042d2b0d603ead142437cc1a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 29 Nov 2024 10:49:32 +0100 Subject: [PATCH 02/55] Deprecate remaining legacy functions and move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler (#4560) * Deprecate legacy functions in `MatrixClient` * Move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler in rust crypto * Remove `olmLib` usage in `MatrixClient` --- src/client.ts | 8 ++++++-- src/crypto-api/CryptoEventHandlerMap.ts | 1 + src/crypto/index.ts | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index 2e72dcf4353..5ef823a298b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -46,7 +46,6 @@ import { noUnsafeEventProps, QueryDict, replaceParam, safeSet, sleep } from "./u import { Direction, EventTimeline } from "./models/event-timeline.ts"; import { IActionsObject, PushProcessor } from "./pushprocessor.ts"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery.ts"; -import * as olmlib from "./crypto/olmlib.ts"; import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64.ts"; import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice.ts"; import { IOlmDevice } from "./crypto/algorithms/megolm.ts"; @@ -252,6 +251,9 @@ export type Store = IStore; export type ResetTimelineCallback = (roomId: string) => boolean; const SCROLLBACK_DELAY_MS = 3000; +/** + * @deprecated Not supported for Rust Cryptography. + */ export const CRYPTO_ENABLED: boolean = isCryptoAvailable(); const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes @@ -2430,6 +2432,8 @@ export class MatrixClient extends TypedEventEmitter { const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); @@ -10115,7 +10119,7 @@ export class MatrixClient extends TypedEventEmitter void; [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void; [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void; + [CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void; } & RustBackupCryptoEventMap; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 8cb04dd5e03..acbb4e318db 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -326,8 +326,6 @@ export type CryptoEventHandlerMap = CryptoApiCryptoEventHandlerMap & { */ [CryptoEvent.Warning]: (type: string) => void; [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void; - - [CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void; }; export class Crypto extends TypedEventEmitter implements CryptoBackend { From 97ef1dc6dff82e45de13d73d606b0c2f0cb00b86 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 29 Nov 2024 10:49:52 +0100 Subject: [PATCH 03/55] Remove deprecated calls of `MatrixClient` (#4563) --- src/models/relations.ts | 6 ++++-- src/models/room.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/models/relations.ts b/src/models/relations.ts index 87a9b7522bb..bb7f05648d7 100644 --- a/src/models/relations.ts +++ b/src/models/relations.ts @@ -20,6 +20,7 @@ import { RelationType } from "../@types/event.ts"; import { TypedEventEmitter } from "./typed-event-emitter.ts"; import { MatrixClient } from "../client.ts"; import { Room } from "./room.ts"; +import { CryptoBackend } from "../common-crypto/CryptoBackend.ts"; export enum RelationsEvent { Add = "Relations.add", @@ -323,8 +324,9 @@ export class Relations extends TypedEventEmitter { * @returns Signals when all events have been decrypted */ public async decryptCriticalEvents(): Promise { - if (!this.client.isCryptoEnabled()) return; + if (!this.client.getCrypto()) return; const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId()!, true); const events = this.getLiveTimeline().getEvents(); @@ -567,7 +567,7 @@ export class Room extends ReadReceipt { * @returns Signals when all events have been decrypted */ public async decryptAllEvents(): Promise { - if (!this.client.isCryptoEnabled()) return; + if (!this.client.getCrypto()) return; const decryptionPromises = this.getUnfilteredTimelineSet() .getLiveTimeline() From edac6a9983bd604c17535a9ae673dc979c7b61c4 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 29 Nov 2024 11:04:20 +0100 Subject: [PATCH 04/55] Replace deprecate imports (#4565) * Replace deprecate imports * Deprecate `CryptoBackend.getEventEncryptionInfo` * Add deprecated alternative --- src/common-crypto/CryptoBackend.ts | 1 + src/rust-crypto/backup.ts | 2 +- src/rust-crypto/libolm_migration.ts | 9 +++++++-- src/testing.ts | 3 +-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index bbd6e5ec682..76fb57aa624 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -78,6 +78,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi { * Get information about the encryption of an event * * @param event - event to be checked + * @deprecated Use {@link CryptoApi#getEncryptionInfoForEvent} instead */ getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo; diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 1b25c313189..69e37d48444 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -30,7 +30,6 @@ import { } from "../crypto-api/keybackup.ts"; import { logger } from "../logger.ts"; import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts"; -import { IMegolmSessionData } from "../crypto/index.ts"; import { TypedEventEmitter } from "../models/typed-event-emitter.ts"; import { encodeUri, logDuration } from "../utils.ts"; import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts"; @@ -38,6 +37,7 @@ import { sleep } from "../utils.ts"; import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts"; import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; +import { IMegolmSessionData } from "../@types/crypto.ts"; /** Authentification of the backup info, depends on algorithm */ type AuthData = KeyBackupInfo["auth_data"]; diff --git a/src/rust-crypto/libolm_migration.ts b/src/rust-crypto/libolm_migration.ts index 4a9784485b1..f23f9d7df41 100644 --- a/src/rust-crypto/libolm_migration.ts +++ b/src/rust-crypto/libolm_migration.ts @@ -21,7 +21,6 @@ import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/s import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts"; import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts"; import { requestKeyBackupVersion } from "./backup.ts"; -import { IRoomEncryption } from "../crypto/RoomList.ts"; import { CrossSigningKeyInfo, Curve25519AuthData } from "../crypto-api/index.ts"; import { RustCrypto } from "./rust-crypto.ts"; import { KeyBackupInfo } from "../crypto-api/keybackup.ts"; @@ -30,6 +29,12 @@ import { encodeBase64 } from "../base64.ts"; import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts"; import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts"; +interface LegacyRoomEncryption { + algorithm: string; + rotation_period_ms?: number; + rotation_period_msgs?: number; +} + /** * Determine if any data needs migrating from the legacy store, and do so. * @@ -375,7 +380,7 @@ export async function migrateRoomSettingsFromLegacyCrypto({ return; } - let rooms: Record = {}; + let rooms: Record = {}; await legacyStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { legacyStore.getEndToEndRooms(txn, (result) => { diff --git a/src/testing.ts b/src/testing.ts index ba609ad0984..eab7e019de3 100644 --- a/src/testing.ts +++ b/src/testing.ts @@ -25,9 +25,8 @@ limitations under the License. import { IContent, IEvent, IUnsigned, MatrixEvent } from "./models/event.ts"; import { RoomMember } from "./models/room-member.ts"; import { EventType } from "./@types/event.ts"; -import { DecryptionError } from "./crypto/algorithms/index.ts"; import { DecryptionFailureCode } from "./crypto-api/index.ts"; -import { EventDecryptionResult } from "./common-crypto/CryptoBackend.ts"; +import { DecryptionError, EventDecryptionResult } from "./common-crypto/CryptoBackend.ts"; /** * Create a {@link MatrixEvent}. From bc5246970c39505bbd2b558f52e0d02bcca1548f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 09:14:45 +0000 Subject: [PATCH 05/55] Extract release sanity checks to reusable workflow (#4546) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/release-checks.yml | 37 ++++++++++++++++++++++++++++ .github/workflows/release-make.yml | 22 ++++------------- 2 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/release-checks.yml diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml new file mode 100644 index 00000000000..1f20acea22c --- /dev/null +++ b/.github/workflows/release-checks.yml @@ -0,0 +1,37 @@ +name: Release Sanity checks +on: + workflow_call: + secrets: + GITHUB_TOKEN: + required: true + inputs: + repository: + type: string + required: true + description: "The repository (in form owner/repo) to check for release blockers" + +permissions: {} +jobs: + checks: + name: Sanity checks + runs-on: ubuntu-24.04 + steps: + - name: Check for X-Release-Blocker label on any open issues or PRs + uses: actions/github-script@v7 + env: + REPO: ${{ inputs.repository }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { REPO } = process.env; + const { data } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${REPO} label:X-Release-Blocker is:open`, + per_page: 50, + }); + + if (data.total_count) { + data.items.forEach(item => { + core.error(`Release blocker: ${item.html_url}`); + }); + core.setFailed(`Found release blockers!`); + } diff --git a/.github/workflows/release-make.yml b/.github/workflows/release-make.yml index 55e07a936fc..a72aacac923 100644 --- a/.github/workflows/release-make.yml +++ b/.github/workflows/release-make.yml @@ -42,26 +42,14 @@ permissions: {} jobs: checks: name: Sanity checks - runs-on: ubuntu-24.04 permissions: issues: read pull-requests: read - steps: - - name: Check for X-Release-Blocker label on any open issues or PRs - uses: actions/github-script@v7 - with: - script: | - const { data } = await github.rest.search.issuesAndPullRequests({ - q: `repo:${context.repo.owner}/${context.repo.repo} label:X-Release-Blocker is:open`, - per_page: 50, - }); - - if (data.total_count) { - data.items.forEach(item => { - core.error(`Release blocker: ${item.html_url}`); - }); - core.setFailed(`Found release blockers!`); - } + uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop + secrets: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + repository: ${{ github.repository }} release: name: Release From 8863e42e350cbb2fe32c22984e875d7012f2d696 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 09:56:52 +0000 Subject: [PATCH 06/55] More typescript linting (#3310) * More typescript linting * Improve types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Discard changes to src/models/MSC3089TreeSpace.ts * Discard changes to src/realtime-callbacks.ts * Fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintrc.cjs | 1 + spec/integ/matrix-client-methods.spec.ts | 22 +++++++++++++++++++ spec/test-utils/webrtc.ts | 3 +++ spec/unit/webrtc/groupCall.spec.ts | 2 +- src/@types/global.d.ts | 2 +- src/autodiscovery.ts | 2 +- src/client.ts | 14 ++++-------- src/crypto/EncryptionSetup.ts | 2 +- src/crypto/dehydration.ts | 6 ++--- src/crypto/store/localStorage-crypto-store.ts | 4 ++-- src/models/event.ts | 8 +++---- src/sliding-sync.ts | 12 +++++----- src/store/indexeddb.ts | 2 +- src/webrtc/call.ts | 6 ++--- src/webrtc/groupCall.ts | 2 +- src/webrtc/stats/callFeedStatsReporter.ts | 7 ++---- src/webrtc/stats/media/mediaTrackHandler.ts | 10 ++------- 17 files changed, 58 insertions(+), 47 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 50ca8d622e7..7bad5c8177a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -138,6 +138,7 @@ module.exports = { tryExtensions: [".ts"], }, ], + "no-extra-boolean-cast": "error", }, }, { diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index b5347e3b559..d5ec9677b19 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -1915,6 +1915,28 @@ describe("MatrixClient", function () { return prom; }); }); + + describe("getDomain", () => { + it("should return null if no userId is set", () => { + const client = new MatrixClient({ baseUrl: "http://localhost" }); + expect(client.getDomain()).toBeNull(); + }); + + it("should return the domain of the userId", () => { + expect(client.getDomain()).toBe("localhost"); + }); + }); + + describe("getUserIdLocalpart", () => { + it("should return null if no userId is set", () => { + const client = new MatrixClient({ baseUrl: "http://localhost" }); + expect(client.getUserIdLocalpart()).toBeNull(); + }); + + it("should return the localpart of the userId", () => { + expect(client.getUserIdLocalpart()).toBe("alice"); + }); + }); }); function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent { diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index fbc6d93be26..924d024cafc 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -481,6 +481,9 @@ export class MockCallMatrixClient extends TypedEventEmitter { - client = new MatrixClient({ baseUrl: "base_url" }); + client = new MatrixClient({ baseUrl: "base_url", userId: "my_user_id" }); jest.spyOn(client, "sendStateEvent").mockResolvedValue({} as any); }); diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 0b51c2b229a..5a280f0e2ce 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -65,6 +65,6 @@ declare global { interface Navigator { // We check for the webkit-prefixed getUserMedia to detect if we're // on webkit: we should check if we still need to do this - webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis; + webkitGetUserMedia?: DummyInterfaceWeShouldntBeUsingThis; } } diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index b321dcddccb..d9243e4daf3 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -130,7 +130,7 @@ export class AutoDiscovery { * configuration, which may include error states. Rejects on unexpected * failure, not when verification fails. */ - public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise { + public static async fromDiscoveryConfig(wellknown?: IClientWellKnown): Promise { // Step 1 is to get the config, which is provided to us here. // We default to an error state to make the first few checks easier to diff --git a/src/client.ts b/src/client.ts index 5ef823a298b..478e2784e51 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1832,10 +1832,7 @@ export class MatrixClient extends TypedEventEmitter { diff --git a/src/crypto/store/localStorage-crypto-store.ts b/src/crypto/store/localStorage-crypto-store.ts index d24e648d634..818a831406d 100644 --- a/src/crypto/store/localStorage-crypto-store.ts +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -162,7 +162,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto func: (session: ISessionInfo) => void, ): void { const sessions = this._getEndToEndSessions(deviceKey); - func(sessions[sessionId] || {}); + func(sessions[sessionId] ?? {}); } public getEndToEndSessions( @@ -170,7 +170,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto txn: unknown, func: (sessions: { [sessionId: string]: ISessionInfo }) => void, ): void { - func(this._getEndToEndSessions(deviceKey) || {}); + func(this._getEndToEndSessions(deviceKey) ?? {}); } public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void { diff --git a/src/models/event.ts b/src/models/event.ts index 5ce7e9fb998..bdb75f5ea2b 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -559,9 +559,9 @@ export class MatrixEvent extends TypedEventEmitter { const list = this.lists.get(key); if (!list || !resp) { @@ -934,7 +934,7 @@ export class SlidingSync extends TypedEventEmitter = new Set(); if (!doNotUpdateList) { for (const [key, list] of Object.entries(resp.lists)) { - list.ops = list.ops || []; + list.ops = list.ops ?? []; if (list.ops.length > 0) { listKeysWithUpdates.add(key); } diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index dfed00df272..8705f4a5c48 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -42,7 +42,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes interface IOpts extends IBaseOpts { /** The Indexed DB interface e.g. `window.indexedDB` */ - indexedDB: IDBFactory; + indexedDB?: IDBFactory; /** Optional database name. The same name must be used to open the same database. */ dbName?: string; /** Optional factory to spin up a Worker to execute the IDB transactions within. */ diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index c2b24cadb82..c1203e24707 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -3016,9 +3016,9 @@ export function supportsMatrixCall(): boolean { // is that the browser throwing a SecurityError will brick the client creation process. try { const supported = Boolean( - window.RTCPeerConnection || - window.RTCSessionDescription || - window.RTCIceCandidate || + window.RTCPeerConnection ?? + window.RTCSessionDescription ?? + window.RTCIceCandidate ?? navigator.mediaDevices, ); if (!supported) { diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 6f0266d4a13..0d2538538f4 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -1445,7 +1445,7 @@ export class GroupCall extends TypedEventEmitter< * Recalculates and updates the participant map to match the room state. */ private updateParticipants(): void { - const localMember = this.room.getMember(this.client.getUserId()!)!; + const localMember = this.room.getMember(this.client.getSafeUserId()); if (!localMember) { // The client hasn't fetched enough of the room state to get our own member // event. This probably shouldn't happen, but sanity check & exit for now. diff --git a/src/webrtc/stats/callFeedStatsReporter.ts b/src/webrtc/stats/callFeedStatsReporter.ts index d219f717277..e78438acbe7 100644 --- a/src/webrtc/stats/callFeedStatsReporter.ts +++ b/src/webrtc/stats/callFeedStatsReporter.ts @@ -49,8 +49,8 @@ export class CallFeedStatsReporter { return { id: track.id, kind: track.kind, - settingDeviceId: settingDeviceId ? settingDeviceId : "unknown", - constrainDeviceId: constrainDeviceId ? constrainDeviceId : "unknown", + settingDeviceId: settingDeviceId ?? "unknown", + constrainDeviceId: constrainDeviceId ?? "unknown", muted: track.muted, enabled: track.enabled, readyState: track.readyState, @@ -63,9 +63,6 @@ export class CallFeedStatsReporter { callFeeds: CallFeed[], prefix = "unknown", ): CallFeedReport { - if (!report.callFeeds) { - report.callFeeds = []; - } callFeeds.forEach((feed) => { const audioTracks = feed.stream.getAudioTracks(); const videoTracks = feed.stream.getVideoTracks(); diff --git a/src/webrtc/stats/media/mediaTrackHandler.ts b/src/webrtc/stats/media/mediaTrackHandler.ts index 31f1264b3ea..8d2108c0b37 100644 --- a/src/webrtc/stats/media/mediaTrackHandler.ts +++ b/src/webrtc/stats/media/mediaTrackHandler.ts @@ -49,18 +49,12 @@ export class MediaTrackHandler { public getLocalTrackIdByMid(mid: string): string | undefined { const transceiver = this.pc.getTransceivers().find((t) => t.mid === mid); - if (transceiver !== undefined && !!transceiver.sender && !!transceiver.sender.track) { - return transceiver.sender.track.id; - } - return undefined; + return transceiver?.sender?.track?.id; } public getRemoteTrackIdByMid(mid: string): string | undefined { const transceiver = this.pc.getTransceivers().find((t) => t.mid === mid); - if (transceiver !== undefined && !!transceiver.receiver && !!transceiver.receiver.track) { - return transceiver.receiver.track.id; - } - return undefined; + return transceiver?.receiver?.track?.id; } public getActiveSimulcastStreams(): number { From 051f4e2ab9b992e67a348312cb7c77031a193ced Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 10:47:35 +0000 Subject: [PATCH 07/55] Fix release-checks to not use reserved name GITHUB_TOKEN Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/release-checks.yml | 9 +++++---- .github/workflows/release-make.yml | 4 ---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index 1f20acea22c..b83b4dd1ce8 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -2,12 +2,13 @@ name: Release Sanity checks on: workflow_call: secrets: - GITHUB_TOKEN: - required: true + ELEMENT_BOT_TOKEN: + required: false inputs: repository: type: string - required: true + required: false + default: ${{ github.repository }} description: "The repository (in form owner/repo) to check for release blockers" permissions: {} @@ -21,7 +22,7 @@ jobs: env: REPO: ${{ inputs.repository }} with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }} script: | const { REPO } = process.env; const { data } = await github.rest.search.issuesAndPullRequests({ diff --git a/.github/workflows/release-make.yml b/.github/workflows/release-make.yml index a72aacac923..421115e68f1 100644 --- a/.github/workflows/release-make.yml +++ b/.github/workflows/release-make.yml @@ -46,10 +46,6 @@ jobs: issues: read pull-requests: read uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop - secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - repository: ${{ github.repository }} release: name: Release From ab78acc7da0754c29abe04c47b63438e2e4caec2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:19:56 +0000 Subject: [PATCH 08/55] Update typedoc (#4568) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 298 ++++++--------------------------------------------- 2 files changed, 36 insertions(+), 264 deletions(-) diff --git a/package.json b/package.json index 009bb295f37..08cf6eefa0a 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "prettier": "3.4.1", "rimraf": "^6.0.0", "ts-node": "^10.9.2", - "typedoc": "^0.26.0", + "typedoc": "^0.27.0", "typedoc-plugin-coverage": "^3.0.0", "typedoc-plugin-mdn-links": "^4.0.0", "typedoc-plugin-missing-exports": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 0684d205c20..4c14db37071 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1163,6 +1163,15 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@gerrit0/mini-shiki@^1.24.0": + version "1.24.1" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.24.1.tgz#60ef10f4e2cfac7a9223e10b88c128438aa44fd8" + integrity sha512-PNP/Gjv3VqU7z7DjRgO3F9Ok5frTKqtpV+LJW1RzMcr2zpRk0ulhEWnbcNGXzPC7BZyWMIHrkfQX2GZRfxrn6Q== + dependencies: + "@shikijs/engine-oniguruma" "^1.24.0" + "@shikijs/types" "^1.24.0" + "@shikijs/vscode-textmate" "^9.3.0" + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -1573,39 +1582,18 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@shikijs/core@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.22.2.tgz#9c22bd4cc8a4d6c062461cfd35e1faa6c617ca25" - integrity sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg== +"@shikijs/engine-oniguruma@^1.24.0": + version "1.24.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz#4e6f49413fbc96dabfa30cb232ca1acf5ca1a446" + integrity sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg== dependencies: - "@shikijs/engine-javascript" "1.22.2" - "@shikijs/engine-oniguruma" "1.22.2" - "@shikijs/types" "1.22.2" + "@shikijs/types" "1.24.0" "@shikijs/vscode-textmate" "^9.3.0" - "@types/hast" "^3.0.4" - hast-util-to-html "^9.0.3" -"@shikijs/engine-javascript@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.22.2.tgz#62e90dbd2ed1d78b972ad7d0a1f8ffaaf5e43279" - integrity sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw== - dependencies: - "@shikijs/types" "1.22.2" - "@shikijs/vscode-textmate" "^9.3.0" - oniguruma-to-js "0.4.3" - -"@shikijs/engine-oniguruma@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.2.tgz#b12a44e3faf486e19fbcf8952f4b56b9b9b8d9b8" - integrity sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA== - dependencies: - "@shikijs/types" "1.22.2" - "@shikijs/vscode-textmate" "^9.3.0" - -"@shikijs/types@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.22.2.tgz#695a283f19963fe0638fc2646862ba5cfc4623a8" - integrity sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg== +"@shikijs/types@1.24.0", "@shikijs/types@^1.24.0": + version "1.24.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.24.0.tgz#a1755b125cb8fb1780a876a0a57242939eafd79f" + integrity sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug== dependencies: "@shikijs/vscode-textmate" "^9.3.0" "@types/hast" "^3.0.4" @@ -1754,7 +1742,7 @@ dependencies: "@types/node" "*" -"@types/hast@^3.0.0", "@types/hast@^3.0.4": +"@types/hast@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== @@ -1802,13 +1790,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/mdast@^4.0.0": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" - integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== - dependencies: - "@types/unist" "*" - "@types/ms@*": version "0.7.34" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" @@ -1853,7 +1834,7 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== -"@types/unist@*", "@types/unist@^3.0.0": +"@types/unist@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== @@ -2001,7 +1982,7 @@ "@typescript-eslint/types" "8.16.0" eslint-visitor-keys "^4.2.0" -"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== @@ -2444,11 +2425,6 @@ caniuse-lite@^1.0.30001669: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz#0eca437bab7d5f03452ff0ef9de8299be6b08e16" integrity sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ== -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - chalk@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" @@ -2481,16 +2457,6 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -2608,11 +2574,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -2804,7 +2765,7 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -dequal@^2.0.0, dequal@^2.0.3: +dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -2814,13 +2775,6 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -devlop@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" - integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== - dependencies: - dequal "^2.0.0" - diff-sequences@^28.1.1: version "28.1.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" @@ -3798,30 +3752,6 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -hast-util-to-html@^9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz#a9999a0ba6b4919576a9105129fead85d37f302b" - integrity sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - ccount "^2.0.0" - comma-separated-tokens "^2.0.0" - hast-util-whitespace "^3.0.0" - html-void-elements "^3.0.0" - mdast-util-to-hast "^13.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - stringify-entities "^4.0.0" - zwitch "^2.0.4" - -hast-util-whitespace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" - integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== - dependencies: - "@types/hast" "^3.0.0" - hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -3839,11 +3769,6 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -html-void-elements@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" - integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== - http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -4973,21 +4898,6 @@ matrix-widget-api@^1.10.0: "@types/events" "^3.0.0" events "^3.2.0" -mdast-util-to-hast@^13.0.0: - version "13.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" - integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - "@ungap/structured-clone" "^1.0.0" - devlop "^1.0.0" - micromark-util-sanitize-uri "^2.0.0" - trim-lines "^3.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - mdurl@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" @@ -5003,38 +4913,6 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromark-util-character@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" - integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== - dependencies: - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-encode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" - integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== - -micromark-util-sanitize-uri@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" - integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-symbol@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" - integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== - -micromark-util-types@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.1.tgz#a3edfda3022c6c6b55bfb049ef5b75d70af50709" - integrity sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ== - micromatch@^4.0.4, micromatch@~4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -5255,13 +5133,6 @@ onetime@^7.0.0: dependencies: mimic-function "^5.0.0" -oniguruma-to-js@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz#8d899714c21f5c7d59a3c0008ca50e848086d740" - integrity sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ== - dependencies: - regex "^4.3.2" - optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -5493,11 +5364,6 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -property-information@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" - integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== - psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -5600,11 +5466,6 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regex@^4.3.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/regex/-/regex-4.4.0.tgz#cb731e2819f230fad69089e1bd854fef7569e90a" - integrity sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ== - regexp-tree@^0.1.27: version "0.1.27" resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" @@ -5852,18 +5713,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shiki@^1.16.2: - version "1.22.2" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.22.2.tgz#ed109a3d0850504ad5a1edf8496470a2121c5b7b" - integrity sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA== - dependencies: - "@shikijs/core" "1.22.2" - "@shikijs/engine-javascript" "1.22.2" - "@shikijs/engine-oniguruma" "1.22.2" - "@shikijs/types" "1.22.2" - "@shikijs/vscode-textmate" "^9.3.0" - "@types/hast" "^3.0.4" - side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -5938,11 +5787,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -6066,14 +5910,6 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -stringify-entities@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" - integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -6241,11 +6077,6 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - ts-api-utils@^1.3.0: version "1.4.2" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.2.tgz#a6a6dff26117ac7965624fc118525971edc6a82a" @@ -6372,25 +6203,25 @@ typedoc-plugin-coverage@^3.0.0: integrity sha512-I8fLeQEERncGn4sUlGZ+B1ehx4L7VRwqa3i6AP+PFfvZK0ToXBGkh9sK7xs8l8FLPXq7Cv0yVy4YCEGgWNzDBw== typedoc-plugin-mdn-links@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.1.tgz#103d6b16bf98bbe2dda21644b67264fd63a6e6f3" - integrity sha512-vt0+5VHvAhdZ02OvfD3O7NySoU+cDEUc5XjApBN4dxCR7CcLk2FqgzKHlDiJjzcsFkLZRvc4Znj2sV8m9AuDsg== + version "4.0.3" + resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.3.tgz#30d22be00bc7689a98c0b223b6a487ff6f338ec7" + integrity sha512-q18V8nXF4MqMBGABPVodfxmU2VLK+C7RpyKgrEGP1oP3MAdavLM8Hmeh7zUJAZ4ky+zotO5ZXfhgChegmaDWug== typedoc-plugin-missing-exports@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-3.1.0.tgz#cab4952c19cae1ab3f91cbbf2d7d17564682b023" integrity sha512-Sogbaj+qDa21NjB3SlIw4JXSwmcl/WOjwiPNaVEcPhpNG/MiRTtpwV81cT7h1cbu9StpONFPbddYWR0KV/fTWA== -typedoc@^0.26.0: - version "0.26.11" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.11.tgz#124b43a5637b7f3237b8c721691b44738c5c9dc9" - integrity sha512-sFEgRRtrcDl2FxVP58Ze++ZK2UQAEvtvvH8rRlig1Ja3o7dDaMHmaBfvJmdGnNEFaLTpQsN8dpvZaTqJSu/Ugw== +typedoc@^0.27.0: + version "0.27.2" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.2.tgz#8a4e0303f4c49174af21e981e0b60e8a637d8167" + integrity sha512-C2ima5TZJHU3ecnRIz50lKd1BsYck5LhYQIy7MRPmjuSEJreUEAt+uAVcZgY7wZsSORzEI7xW8miZIdxv/cbmw== dependencies: + "@gerrit0/mini-shiki" "^1.24.0" lunr "^2.3.9" markdown-it "^14.1.0" minimatch "^9.0.5" - shiki "^1.16.2" - yaml "^2.5.1" + yaml "^2.6.1" typescript@^5.4.2: version "5.6.3" @@ -6445,44 +6276,6 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unist-util-is@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" - integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" - integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-stringify-position@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" - integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-visit-parents@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" - integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - -unist-util-visit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" - integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -6543,22 +6336,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - -vfile@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" - integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== - dependencies: - "@types/unist" "^3.0.0" - vfile-message "^4.0.0" - w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" @@ -6756,10 +6533,10 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^2.5.1: - version "2.6.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" - integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== +yaml@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" + integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== yaml@~2.5.0: version "2.5.1" @@ -6803,8 +6580,3 @@ zod@^3.22.4: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== - -zwitch@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== From b8e332b53d0d36cfc019fefffe7fc76bba11df65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:47:14 +0000 Subject: [PATCH 09/55] Update dependency typescript to v5.7.2 (#4553) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4c14db37071..2cfe41e0730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6224,9 +6224,9 @@ typedoc@^0.27.0: yaml "^2.6.1" typescript@^5.4.2: - version "5.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" - integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" From d3f5526ec07dcbd0e4677a56b5e53f7d3a4cbcc2 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 3 Dec 2024 12:23:40 +0000 Subject: [PATCH 10/55] v34.13.0 --- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fdd431444..d2687d1bdd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +Changes in [34.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.13.0) (2024-12-03) +==================================================================================================== +## 🦖 Deprecations + +* Deprecate `MatrixClient.isEventSenderVerified` ([#4527](https://github.com/matrix-org/matrix-js-sdk/pull/4527)). Contributed by @florianduros. +* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros. + +## ✨ Features + +* Ensure we disambiguate display names which look like MXIDs ([#4540](https://github.com/matrix-org/matrix-js-sdk/pull/4540)). Contributed by @t3chguy. +* Add `CryptoApi.getBackupInfo` ([#4512](https://github.com/matrix-org/matrix-js-sdk/pull/4512)). Contributed by @florianduros. +* Fix local echo in embedded mode ([#4498](https://github.com/matrix-org/matrix-js-sdk/pull/4498)). Contributed by @toger5. +* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros. + +## 🐛 Bug Fixes + +* Fix `RustBackupManager` remaining values after current backup removal ([#4537](https://github.com/matrix-org/matrix-js-sdk/pull/4537)). Contributed by @florianduros. + + Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19) ==================================================================================================== ## 🦖 Deprecations diff --git a/package.json b/package.json index 6ebe111f056..acf66a0c72d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "34.13.0-rc.0", + "version": "34.13.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=20.0.0" From 1cad6f445198cb8e0da4e9cec04ec7378f0ee6f8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:38:48 +0000 Subject: [PATCH 11/55] Add multiprocess health warnings to `initRustCrypto` (#4571) --- README.md | 2 ++ src/client.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 756b8c71ca9..5272706e432 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,8 @@ await matrixClient.initRustCrypto(); After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto). +**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time. + ## Secret storage You should normally set up [secret storage](https://spec.matrix.org/v1.12/client-server-api/#secret-storage) before using the end-to-end encryption. To do this, call [`CryptoApi.bootstrapSecretStorage`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapSecretStorage). diff --git a/src/client.ts b/src/client.ts index 478e2784e51..dbe1363942c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2222,6 +2222,10 @@ export class MatrixClient extends TypedEventEmitter Date: Wed, 4 Dec 2024 22:32:09 +0000 Subject: [PATCH 12/55] Avoid use of Buffer as it does not exist in the Web natively (#4569) --- spec/integ/crypto/verification.spec.ts | 32 ++++---- spec/unit/base64.spec.ts | 26 +------ spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 16 ++-- src/@types/global.d.ts | 20 +++++ src/base64.ts | 78 +++++++++---------- src/client.ts | 8 +- src/common-crypto/CryptoBackend.ts | 2 +- src/crypto-api/recovery-key.ts | 2 +- src/crypto-api/verification.ts | 6 +- src/crypto/index.ts | 2 +- .../request/VerificationRequest.ts | 9 ++- src/digest.ts | 6 +- src/rust-crypto/rust-crypto.ts | 4 +- src/rust-crypto/verification.ts | 10 +-- src/utils/encryptAESSecretStorageItem.ts | 4 +- 15 files changed, 112 insertions(+), 113 deletions(-) diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index a9f9ba522e7..7b207a432da 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -473,21 +473,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st expect(request.phase).toEqual(VerificationPhase.Ready); // we should now have QR data we can display - const qrCodeBuffer = (await request.generateQRCode())!; - expect(qrCodeBuffer).toBeTruthy(); + const rawQrCodeBuffer = (await request.generateQRCode())!; + expect(rawQrCodeBuffer).toBeTruthy(); + const qrCodeBuffer = new Uint8Array(rawQrCodeBuffer); + const textDecoder = new TextDecoder(); // https://spec.matrix.org/v1.7/client-server-api/#qr-code-format - expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX"); - expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version - expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode - const txnIdLen = qrCodeBuffer.readUint16BE(8); - expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId); + expect(textDecoder.decode(qrCodeBuffer.slice(0, 6))).toEqual("MATRIX"); + expect(qrCodeBuffer[6]).toEqual(0x02); // version + expect(qrCodeBuffer[7]).toEqual(0x02); // mode + const txnIdLen = (qrCodeBuffer[8] << 8) + qrCodeBuffer[9]; + expect(textDecoder.decode(qrCodeBuffer.slice(10, 10 + txnIdLen))).toEqual(transactionId); // Alice's device's public key comes next, but we have nothing to do with it here. - // const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen); - expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual( - Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"), + // const aliceDevicePubKey = qrCodeBuffer.slice(10 + txnIdLen, 32 + 10 + txnIdLen); + expect(encodeUnpaddedBase64(qrCodeBuffer.slice(42 + txnIdLen, 32 + 42 + txnIdLen))).toEqual( + MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, ); - const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen); + const sharedSecret = qrCodeBuffer.slice(74 + txnIdLen); // we should still be "Ready" and have no verifier expect(request.phase).toEqual(VerificationPhase.Ready); @@ -805,7 +807,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st // we should now have QR data we can display const qrCodeBuffer = (await request.generateQRCode())!; expect(qrCodeBuffer).toBeTruthy(); - const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length); + const sharedSecret = qrCodeBuffer.slice(74 + transactionId.length); // the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start" returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret)); @@ -1627,7 +1629,7 @@ function buildReadyMessage( } /** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */ -function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) { +function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) { return { type: "m.key.verification.start", content: { @@ -1723,7 +1725,7 @@ function buildQRCode( key2Base64: string, sharedSecret: string, mode = 0x02, -): Uint8Array { +): Uint8ClampedArray { // https://spec.matrix.org/v1.7/client-server-api/#qr-code-format const qrCodeBuffer = Buffer.alloc(150); // oversize @@ -1739,5 +1741,5 @@ function buildQRCode( idx += qrCodeBuffer.write(sharedSecret, idx); // truncate to the right length - return qrCodeBuffer.subarray(0, idx); + return new Uint8ClampedArray(qrCodeBuffer.subarray(0, idx)); } diff --git a/spec/unit/base64.spec.ts b/spec/unit/base64.spec.ts index cba73bec9f3..69a64399d27 100644 --- a/spec/unit/base64.spec.ts +++ b/spec/unit/base64.spec.ts @@ -15,34 +15,10 @@ limitations under the License. */ import { TextEncoder, TextDecoder } from "util"; -import NodeBuffer from "node:buffer"; import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64"; -describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => { - let origBuffer = Buffer; - - beforeAll(() => { - if (env === "browser") { - origBuffer = Buffer; - // @ts-ignore - // eslint-disable-next-line no-global-assign - Buffer = undefined; - - globalThis.atob = NodeBuffer.atob; - globalThis.btoa = NodeBuffer.btoa; - } - }); - - afterAll(() => { - // eslint-disable-next-line no-global-assign - Buffer = origBuffer; - // @ts-ignore - globalThis.atob = undefined; - // @ts-ignore - globalThis.btoa = undefined; - }); - +describe("Base64 encoding", () => { it("Should decode properly encoded data", () => { const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ=")); diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 26368e0b33f..248be4c19ec 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -38,6 +38,8 @@ const membershipTemplate: CallMembershipData = { const mockFocus = { type: "mock" }; +const textEncoder = new TextEncoder(); + describe("MatrixRTCSession", () => { let client: MatrixClient; let sess: MatrixRTCSession | undefined; @@ -1345,7 +1347,7 @@ describe("MatrixRTCSession", () => { sess!.reemitEncryptionKeys(); expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1); expect(encryptionKeyChangedListener).toHaveBeenCalledWith( - Buffer.from("this is the key", "utf-8"), + textEncoder.encode("this is the key"), 0, "@bob:example.org:bobsphone", ); @@ -1377,7 +1379,7 @@ describe("MatrixRTCSession", () => { sess!.reemitEncryptionKeys(); expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1); expect(encryptionKeyChangedListener).toHaveBeenCalledWith( - Buffer.from("this is the key", "utf-8"), + textEncoder.encode("this is the key"), 4, "@bob:example.org:bobsphone", ); @@ -1409,7 +1411,7 @@ describe("MatrixRTCSession", () => { sess!.reemitEncryptionKeys(); expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1); expect(encryptionKeyChangedListener).toHaveBeenCalledWith( - Buffer.from("this is the key", "utf-8"), + textEncoder.encode("this is the key"), 0, "@bob:example.org:bobsphone", ); @@ -1436,12 +1438,12 @@ describe("MatrixRTCSession", () => { sess!.reemitEncryptionKeys(); expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2); expect(encryptionKeyChangedListener).toHaveBeenCalledWith( - Buffer.from("this is the key", "utf-8"), + textEncoder.encode("this is the key"), 0, "@bob:example.org:bobsphone", ); expect(encryptionKeyChangedListener).toHaveBeenCalledWith( - Buffer.from("this is the key", "utf-8"), + textEncoder.encode("this is the key"), 4, "@bob:example.org:bobsphone", ); @@ -1489,7 +1491,7 @@ describe("MatrixRTCSession", () => { sess!.reemitEncryptionKeys(); expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1); expect(encryptionKeyChangedListener).toHaveBeenCalledWith( - Buffer.from("newer key", "utf-8"), + textEncoder.encode("newer key"), 0, "@bob:example.org:bobsphone", ); @@ -1537,7 +1539,7 @@ describe("MatrixRTCSession", () => { sess!.reemitEncryptionKeys(); expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1); expect(encryptionKeyChangedListener).toHaveBeenCalledWith( - Buffer.from("second key", "utf-8"), + textEncoder.encode("second key"), 0, "@bob:example.org:bobsphone", ); diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 5a280f0e2ce..c9103dbebf1 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -67,4 +67,24 @@ declare global { // on webkit: we should check if we still need to do this webkitGetUserMedia?: DummyInterfaceWeShouldntBeUsingThis; } + + export interface Uint8ArrayToBase64Options { + alphabet?: "base64" | "base64url"; + omitPadding?: boolean; + } + + interface Uint8Array { + // https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64 + toBase64?(options?: Uint8ArrayToBase64Options): string; + } + + export interface Uint8ArrayFromBase64Options { + alphabet?: "base64"; // Our fallback code only handles base64. + lastChunkHandling?: "loose"; // Our fallback code doesn't support other handling at this time. + } + + interface Uint8ArrayConstructor { + // https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64 + fromBase64?(base64: string, options?: Uint8ArrayFromBase64Options): Uint8Array; + } } diff --git a/src/base64.ts b/src/base64.ts index 450468b0889..4cb876b876d 100644 --- a/src/base64.ts +++ b/src/base64.ts @@ -18,30 +18,32 @@ limitations under the License. * Base64 encoding and decoding utilities */ +function toBase64(uint8Array: Uint8Array, options: Uint8ArrayToBase64Options): string { + if (typeof uint8Array.toBase64 === "function") { + // Currently this is only supported in Firefox, + // but we match the options in the hope in the future we can rely on it for all environments. + // https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64 + return uint8Array.toBase64(options); + } + + let base64 = btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), "")); + if (options.omitPadding) { + base64 = base64.replace(/={1,2}$/, ""); + } + if (options.alphabet === "base64url") { + base64 = base64.replace(/\+/g, "-").replace(/\//g, "_"); + } + + return base64; +} + /** * Encode a typed array of uint8 as base64. * @param uint8Array - The data to encode. * @returns The base64. */ -export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { - // A brief note on the state of base64 encoding in Javascript. - // As of 2023, there is still no common native impl between both browsers and - // node. Older Webpack provides an impl for Buffer and there is a polyfill class - // for it. There are also plenty of pure js impls, eg. base64-js which has 2336 - // dependents at current count. Using this would probably be fine although it's - // a little under-docced and run by an individual. The node impl works fine, - // the browser impl works but predates Uint8Array and so only uses strings. - // Right now, switching between native (or polyfilled) impls like this feels - // like the least bad option, but... *shrugs*. - if (typeof Buffer === "function") { - return Buffer.from(uint8Array).toString("base64"); - } else if (typeof btoa === "function" && uint8Array instanceof Uint8Array) { - // ArrayBuffer is a node concept so the param should always be a Uint8Array on - // the browser. We need to check because ArrayBuffers don't have reduce. - return btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), "")); - } else { - throw new Error("No base64 impl found!"); - } +export function encodeBase64(uint8Array: Uint8Array): string { + return toBase64(uint8Array, { alphabet: "base64", omitPadding: false }); } /** @@ -49,8 +51,8 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { * @param uint8Array - The data to encode. * @returns The unpadded base64. */ -export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string { - return encodeBase64(uint8Array).replace(/={1,2}$/, ""); +export function encodeUnpaddedBase64(uint8Array: Uint8Array): string { + return toBase64(uint8Array, { alphabet: "base64", omitPadding: true }); } /** @@ -58,8 +60,19 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri * @param uint8Array - The data to encode. * @returns The unpadded base64. */ -export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string { - return encodeUnpaddedBase64(uint8Array).replace(/\+/g, "-").replace(/\//g, "_"); +export function encodeUnpaddedBase64Url(uint8Array: Uint8Array): string { + return toBase64(uint8Array, { alphabet: "base64url", omitPadding: true }); +} + +function fromBase64(base64: string, options: Uint8ArrayFromBase64Options): Uint8Array { + if (typeof Uint8Array.fromBase64 === "function") { + // Currently this is only supported in Firefox, + // but we match the options in the hope in the future we can rely on it for all environments. + // https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64 + return Uint8Array.fromBase64(base64, options); + } + + return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); } /** @@ -68,21 +81,6 @@ export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): s * @returns The decoded data. */ export function decodeBase64(base64: string): Uint8Array { - // See encodeBase64 for a short treatise on base64 en/decoding in JS - if (typeof Buffer === "function") { - return Buffer.from(base64, "base64"); - } else if (typeof atob === "function") { - const itFunc = function* (): Generator { - const decoded = atob( - // built-in atob doesn't support base64url: convert so we support either - base64.replace(/-/g, "+").replace(/_/g, "/"), - ); - for (let i = 0; i < decoded.length; ++i) { - yield decoded.charCodeAt(i); - } - }; - return Uint8Array.from(itFunc()); - } else { - throw new Error("No base64 impl found!"); - } + // The function requires us to select an alphabet, but we don't know if base64url was used so we convert. + return fromBase64(base64.replace(/-/g, "+").replace(/_/g, "/"), { alphabet: "base64", lastChunkHandling: "loose" }); } diff --git a/src/client.ts b/src/client.ts index dbe1363942c..a13eceebcc4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3899,28 +3899,28 @@ export class MatrixClient extends TypedEventEmitter, + privKey: Uint8Array, targetRoomId: undefined, targetSessionId: undefined, backupInfo: IKeyBackupInfo, opts?: IKeyBackupRestoreOpts, ): Promise; private async restoreKeyBackup( - privKey: ArrayLike, + privKey: Uint8Array, targetRoomId: string, targetSessionId: undefined, backupInfo: IKeyBackupInfo, opts?: IKeyBackupRestoreOpts, ): Promise; private async restoreKeyBackup( - privKey: ArrayLike, + privKey: Uint8Array, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupInfo, opts?: IKeyBackupRestoreOpts, ): Promise; private async restoreKeyBackup( - privKey: ArrayLike, + privKey: Uint8Array, targetRoomId: string | undefined, targetSessionId: string | undefined, backupInfo: IKeyBackupInfo, diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index 76fb57aa624..85007d06783 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -108,7 +108,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi { * @param backupInfo - The backup information * @param privKey - The private decryption key. */ - getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike): Promise; + getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise; /** * Import a list of room keys restored from backup diff --git a/src/crypto-api/recovery-key.ts b/src/crypto-api/recovery-key.ts index 4b27b5a8ef8..2c4abdfdfb9 100644 --- a/src/crypto-api/recovery-key.ts +++ b/src/crypto-api/recovery-key.ts @@ -26,7 +26,7 @@ const KEY_SIZE = 32; * @param key */ export function encodeRecoveryKey(key: ArrayLike): string | undefined { - const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1); + const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1); buf.set(OLM_RECOVERY_KEY_PREFIX, 0); buf.set(key, OLM_RECOVERY_KEY_PREFIX.length); diff --git a/src/crypto-api/verification.ts b/src/crypto-api/verification.ts index 468a5d7a9f2..a5bf5902130 100644 --- a/src/crypto-api/verification.ts +++ b/src/crypto-api/verification.ts @@ -155,7 +155,7 @@ export interface VerificationRequest * @param qrCodeData - the decoded QR code. * @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow. */ - scanQRCode(qrCodeData: Uint8Array): Promise; + scanQRCode(qrCodeData: Uint8ClampedArray): Promise; /** * The verifier which is doing the actual verification, once the method has been established. @@ -170,7 +170,7 @@ export interface VerificationRequest * * @deprecated Not supported in Rust Crypto. Use {@link VerificationRequest#generateQRCode} instead. */ - getQRCodeBytes(): Buffer | undefined; + getQRCodeBytes(): Uint8ClampedArray | undefined; /** * Generate the data for a QR code allowing the other device to verify this one, if it supports it. @@ -178,7 +178,7 @@ export interface VerificationRequest * Only returns data once `phase` is {@link VerificationPhase.Ready} and the other party can scan a QR code; * otherwise returns `undefined`. */ - generateQRCode(): Promise; + generateQRCode(): Promise; /** * If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling diff --git a/src/crypto/index.ts b/src/crypto/index.ts index acbb4e318db..557c1f5cad0 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1846,7 +1846,7 @@ export class Crypto extends TypedEventEmitter): Promise { + public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise { if (!(privKey instanceof Uint8Array)) { throw new Error(`getBackupDecryptor expects Uint8Array`); } diff --git a/src/crypto/verification/request/VerificationRequest.ts b/src/crypto/verification/request/VerificationRequest.ts index 8a3aca9a287..ac6111d2605 100644 --- a/src/crypto/verification/request/VerificationRequest.ts +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -281,8 +281,9 @@ export class VerificationRequest { + public async generateQRCode(): Promise { return this.getQRCodeBytes(); } @@ -478,7 +479,7 @@ export class VerificationRequest { + public scanQRCode(qrCodeData: Uint8ClampedArray): Promise { throw new Error("QR code scanning not supported by legacy crypto"); } diff --git a/src/digest.ts b/src/digest.ts index 85b5be9643b..102349f6437 100644 --- a/src/digest.ts +++ b/src/digest.ts @@ -18,11 +18,11 @@ limitations under the License. * Computes a SHA-256 hash of a string (after utf-8 encoding) and returns it as an ArrayBuffer. * * @param plaintext The string to hash - * @returns An ArrayBuffer containing the SHA-256 hash of the input string + * @returns An Uint8Array containing the SHA-256 hash of the input string * @throws If the subtle crypto API is not available, for example if the code is running * in a web page with an insecure context (eg. served over plain HTTP). */ -export async function sha256(plaintext: string): Promise { +export async function sha256(plaintext: string): Promise { if (!globalThis.crypto.subtle) { throw new Error("Crypto.subtle is not available: insecure context?"); } @@ -30,5 +30,5 @@ export async function sha256(plaintext: string): Promise { const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8); - return digest; + return new Uint8Array(digest); } diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index d18461e7b22..86853a9867a 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -329,7 +329,7 @@ export class RustCrypto extends TypedEventEmitter): Promise { + public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise { if (!(privKey instanceof Uint8Array)) { throw new Error(`getBackupDecryptor: expects Uint8Array`); } @@ -1178,7 +1178,7 @@ export class RustCrypto extends TypedEventEmitter { const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys(); if (!backupKeys.decryptionKey) return null; - return Buffer.from(backupKeys.decryptionKey.toBase64(), "base64"); + return decodeBase64(backupKeys.decryptionKey.toBase64()); } /** diff --git a/src/rust-crypto/verification.ts b/src/rust-crypto/verification.ts index 284bcc61e64..fe25ac54d9b 100644 --- a/src/rust-crypto/verification.ts +++ b/src/rust-crypto/verification.ts @@ -381,8 +381,8 @@ export class RustVerificationRequest * @param qrCodeData - the decoded QR code. * @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow. */ - public async scanQRCode(uint8Array: Uint8Array): Promise { - const scan = RustSdkCryptoJs.QrCodeScan.fromBytes(new Uint8ClampedArray(uint8Array)); + public async scanQRCode(uint8Array: Uint8ClampedArray): Promise { + const scan = RustSdkCryptoJs.QrCodeScan.fromBytes(uint8Array); const verifier: RustSdkCryptoJs.Qr = await this.inner.scanQrCode(scan); // this should have triggered the onChange callback, and we should now have a verifier @@ -416,7 +416,7 @@ export class RustVerificationRequest /** * Stub implementation of {@link Crypto.VerificationRequest#getQRCodeBytes}. */ - public getQRCodeBytes(): Buffer | undefined { + public getQRCodeBytes(): Uint8ClampedArray | undefined { throw new Error("getQRCodeBytes() unsupported in Rust Crypto; use generateQRCode() instead."); } @@ -425,7 +425,7 @@ export class RustVerificationRequest * * Implementation of {@link Crypto.VerificationRequest#generateQRCode}. */ - public async generateQRCode(): Promise { + public async generateQRCode(): Promise { // make sure that we have a list of the other user's devices (workaround https://github.com/matrix-org/matrix-rust-sdk/issues/2896) if (!(await this.getOtherDevice())) { throw new Error("generateQRCode(): other device is unknown"); @@ -435,7 +435,7 @@ export class RustVerificationRequest // If we are unable to generate a QRCode, we return undefined if (!innerVerifier) return; - return Buffer.from(innerVerifier.toBytes()); + return innerVerifier.toBytes(); } /** diff --git a/src/utils/encryptAESSecretStorageItem.ts b/src/utils/encryptAESSecretStorageItem.ts index 064e914a125..3592ff48855 100644 --- a/src/utils/encryptAESSecretStorageItem.ts +++ b/src/utils/encryptAESSecretStorageItem.ts @@ -67,7 +67,7 @@ export default async function encryptAESSecretStorageItem( return { iv: encodeBase64(iv), - ciphertext: encodeBase64(ciphertext), - mac: encodeBase64(hmac), + ciphertext: encodeBase64(new Uint8Array(ciphertext)), + mac: encodeBase64(new Uint8Array(hmac)), }; } From c54ca29aa884fb7a6eef56001bb952b7d2039810 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 5 Dec 2024 12:08:38 +0100 Subject: [PATCH 13/55] Rename `initCrypto` into `initLegacyCrypto` (#4567) --- README.md | 6 ++-- spec/integ/crypto/crypto.spec.ts | 6 ++-- spec/integ/crypto/olm-encryption-spec.ts | 4 +-- spec/integ/devicelist-integ.spec.ts | 2 +- spec/integ/matrix-client-methods.spec.ts | 4 +-- spec/integ/matrix-client-syncing.spec.ts | 6 ++-- spec/integ/sliding-sync-sdk.spec.ts | 2 +- spec/test-utils/test-utils.ts | 2 +- spec/unit/crypto.spec.ts | 32 +++++++++++----------- spec/unit/crypto/algorithms/megolm.spec.ts | 16 +++++++---- spec/unit/crypto/backup.spec.ts | 16 +++++------ spec/unit/crypto/cross-signing.spec.ts | 2 +- spec/unit/crypto/dehydration.spec.ts | 2 +- spec/unit/crypto/secrets.spec.ts | 2 +- spec/unit/crypto/verification/util.ts | 2 +- src/client.ts | 14 +++++----- 16 files changed, 61 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 5272706e432..e3bf79204a0 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,7 @@ Then visit `http://localhost:8005` to see the API docs. ## Initialization -**Do not use `matrixClient.initCrypto()`. This method is deprecated and no longer maintained.** +**Do not use `matrixClient.initLegacyCrypto()`. This method is deprecated and no longer maintained.** To initialize the end-to-end encryption support in the matrix client: @@ -398,10 +398,10 @@ Once the cross-signing is set up on one of your devices, you can verify another ## Migrating from the legacy crypto stack to Rust crypto -If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initCrypto()`), you will +If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initLegacyCrypto()`), you will need to migrate existing devices to the Rust crypto stack. -This migration happens automatically when you call `initRustCrypto()` instead of `initCrypto()`, +This migration happens automatically when you call `initRustCrypto()` instead of `initLegacyCrypto()`, but you need to provide the legacy [`cryptoStore`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#cryptoStore) and [`pickleKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#pickleKey) to [`createClient`](https://matrix-org.github.io/matrix-js-sdk/functions/matrix.createClient.html): ```javascript diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 5b219d1d51c..96d809aea99 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -1741,7 +1741,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, groupSession: groupSession, room_id: ROOM_ID, }); - await testClient.client.initCrypto(); + await testClient.client.initLegacyCrypto(); const keys = [ { room_id: ROOM_ID, @@ -1853,7 +1853,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, oldBackendOnly("Alice receives shared history before being invited to a room by the sharer", async () => { const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux"); - await beccaTestClient.client.initCrypto(); + await beccaTestClient.client.initLegacyCrypto(); expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); @@ -2007,7 +2007,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, oldBackendOnly("Alice receives shared history before being invited to a room by someone else", async () => { const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux"); - await beccaTestClient.client.initCrypto(); + await beccaTestClient.client.initLegacyCrypto(); expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await startClientAndAwaitFirstSync(); diff --git a/spec/integ/crypto/olm-encryption-spec.ts b/spec/integ/crypto/olm-encryption-spec.ts index 5977bda74ef..5b98c63936a 100644 --- a/spec/integ/crypto/olm-encryption-spec.ts +++ b/spec/integ/crypto/olm-encryption-spec.ts @@ -345,10 +345,10 @@ describe("MatrixClient crypto", () => { beforeEach(async () => { aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken); - await aliTestClient.client.initCrypto(); + await aliTestClient.client.initLegacyCrypto(); bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken); - await bobTestClient.client.initCrypto(); + await bobTestClient.client.initLegacyCrypto(); aliMessages = []; bobMessages = []; diff --git a/spec/integ/devicelist-integ.spec.ts b/spec/integ/devicelist-integ.spec.ts index 88388fdf9f7..ce741d8dc39 100644 --- a/spec/integ/devicelist-integ.spec.ts +++ b/spec/integ/devicelist-integ.spec.ts @@ -77,7 +77,7 @@ describe("DeviceList management:", function () { async function createTestClient() { const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend); - await testClient.client.initCrypto(); + await testClient.client.initLegacyCrypto(); return testClient; } diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index d5ec9677b19..e058426cbd7 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -650,9 +650,9 @@ describe("MatrixClient", function () { } beforeEach(function () { - // running initCrypto should trigger a key upload + // running initLegacyCrypto should trigger a key upload httpBackend.when("POST", "/keys/upload").respond(200, {}); - return Promise.all([client.initCrypto(), httpBackend.flush("/keys/upload", 1)]); + return Promise.all([client.initLegacyCrypto(), httpBackend.flush("/keys/upload", 1)]); }); afterEach(() => { diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index d3156f0fb12..2c0ab315bed 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -112,7 +112,7 @@ describe("MatrixClient syncing", () => { }); it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => { - await client!.initCrypto(); + await client!.initLegacyCrypto(); const roomId = "!cycles:example.org"; @@ -227,7 +227,7 @@ describe("MatrixClient syncing", () => { }); it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => { - await client!.initCrypto(); + await client!.initLegacyCrypto(); const roomId = "!cycles:example.org"; @@ -2573,7 +2573,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => { idbHttpBackend.when("GET", "/pushrules/").respond(200, {}); idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); - await idbClient.initCrypto(); + await idbClient.initLegacyCrypto(); const roomId = "!invite:example.org"; diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index f6264530088..7b080351a6f 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -119,7 +119,7 @@ describe("SlidingSyncSdk", () => { mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0)); if (testOpts.withCrypto) { httpBackend!.when("GET", "/room_keys/version").respond(404, {}); - await client!.initCrypto(); + await client!.initLegacyCrypto(); syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto; } httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {}); diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index 22e7006e47c..d0c9abb2a5d 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -561,7 +561,7 @@ export type InitCrypto = (_: MatrixClient) => Promise; CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto(); if (globalThis.Olm) { - CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto(); + CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initLegacyCrypto(); } export const emitPromise = (e: EventEmitter, k: string): Promise => new Promise((r) => e.once(k, r)); diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index ce2d2389212..419bb530a66 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -119,7 +119,7 @@ describe("Crypto", function () { it("getVersion() should return the current version of the olm library", async () => { const client = new TestClient("@alice:example.com", "deviceid").client; - await client.initCrypto(); + await client.initLegacyCrypto(); const olmVersionTuple = Crypto.getOlmVersion(); expect(client.getCrypto()?.getVersion()).toBe( @@ -130,7 +130,7 @@ describe("Crypto", function () { describe("encrypted events", function () { it("provides encryption information for events from unverified senders", async function () { const client = new TestClient("@alice:example.com", "deviceid").client; - await client.initCrypto(); + await client.initLegacyCrypto(); // unencrypted event const event = { @@ -210,7 +210,7 @@ describe("Crypto", function () { let client: MatrixClient; beforeEach(async () => { client = new TestClient("@alice:example.com", "deviceid").client; - await client.initCrypto(); + await client.initLegacyCrypto(); // mock out the verification check client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false); @@ -306,7 +306,7 @@ describe("Crypto", function () { it("doesn't throw an error when attempting to decrypt a redacted event", async () => { const client = new TestClient("@alice:example.com", "deviceid").client; - await client.initCrypto(); + await client.initLegacyCrypto(); const event = new MatrixEvent({ content: {}, @@ -439,10 +439,10 @@ describe("Crypto", function () { secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client; bobClient = new TestClient("@bob:example.com", "bobdevice").client; claraClient = new TestClient("@clara:example.com", "claradevice").client; - await aliceClient.initCrypto(); - await secondAliceClient.initCrypto(); - await bobClient.initCrypto(); - await claraClient.initCrypto(); + await aliceClient.initLegacyCrypto(); + await secondAliceClient.initLegacyCrypto(); + await bobClient.initLegacyCrypto(); + await claraClient.initLegacyCrypto(); }); afterEach(async function () { @@ -1111,7 +1111,7 @@ describe("Crypto", function () { jest.spyOn(logger, "debug").mockImplementation(() => {}); jest.setTimeout(10000); const client = new TestClient("@a:example.com", "dev").client; - await client.initCrypto(); + await client.initLegacyCrypto(); client.crypto!.isCrossSigningReady = async () => false; client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null); client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null); @@ -1147,9 +1147,9 @@ describe("Crypto", function () { client = new TestClient("@alice:example.org", "aliceweb"); - // running initCrypto should trigger a key upload + // running initLegacyCrypto should trigger a key upload client.httpBackend.when("POST", "/keys/upload").respond(200, {}); - await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]); + await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]); encryptedPayload = { algorithm: "m.olm.v1.curve25519-aes-sha2", @@ -1264,9 +1264,9 @@ describe("Crypto", function () { client = new TestClient("@alice:example.org", "aliceweb"); - // running initCrypto should trigger a key upload + // running initLegacyCrypto should trigger a key upload client.httpBackend.when("POST", "/keys/upload").respond(200, {}); - await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]); + await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]); encryptedPayload = { algorithm: "m.olm.v1.curve25519-aes-sha2", @@ -1362,7 +1362,7 @@ describe("Crypto", function () { beforeEach(async () => { client = new TestClient("@alice:example.org", "aliceweb"); - await client.client.initCrypto(); + await client.client.initLegacyCrypto(); }); afterEach(async () => { @@ -1388,7 +1388,7 @@ describe("Crypto", function () { beforeEach(async () => { client = new TestClient("@alice:example.org", "aliceweb"); - await client.client.initCrypto(); + await client.client.initLegacyCrypto(); }); afterEach(async () => { @@ -1414,7 +1414,7 @@ describe("Crypto", function () { beforeEach(async () => { client = new TestClient("@alice:example.org", "aliceweb"); - await client.client.initCrypto(); + await client.client.initLegacyCrypto(); }); afterEach(async function () { diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index d2a1fbf3372..20d72702110 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -610,7 +610,11 @@ describe("MegolmDecryption", function () { const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client; const bobClient2 = new TestClient("@bob:example.com", "bobdevice2").client; - await Promise.all([aliceClient.initCrypto(), bobClient1.initCrypto(), bobClient2.initCrypto()]); + await Promise.all([ + aliceClient.initLegacyCrypto(), + bobClient1.initLegacyCrypto(), + bobClient2.initLegacyCrypto(), + ]); const aliceDevice = aliceClient.crypto!.olmDevice; const bobDevice1 = bobClient1.crypto!.olmDevice; const bobDevice2 = bobClient2.crypto!.olmDevice; @@ -704,7 +708,7 @@ describe("MegolmDecryption", function () { it("does not block unverified devices when sending verification events", async function () { const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]); + await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); const bobDevice = bobClient.crypto!.olmDevice; const encryptionCfg = { @@ -789,7 +793,7 @@ describe("MegolmDecryption", function () { it("notifies devices when unable to create olm session", async function () { const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]); + await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); const aliceDevice = aliceClient.crypto!.olmDevice; const bobDevice = bobClient.crypto!.olmDevice; @@ -873,7 +877,7 @@ describe("MegolmDecryption", function () { it("throws an error describing why it doesn't have a key", async function () { const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]); + await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); const bobDevice = bobClient.crypto!.olmDevice; const aliceEventEmitter = new TypedEventEmitter(); @@ -955,7 +959,7 @@ describe("MegolmDecryption", function () { it("throws an error describing the lack of an olm session", async function () { const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]); + await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); const aliceEventEmitter = new TypedEventEmitter(); aliceClient.crypto!.registerEventHandlers(aliceEventEmitter); @@ -1051,7 +1055,7 @@ describe("MegolmDecryption", function () { it("throws an error to indicate a wedged olm session", async function () { const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; const bobClient = new TestClient("@bob:example.com", "bobdevice").client; - await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]); + await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]); const aliceEventEmitter = new TypedEventEmitter(); aliceClient.crypto!.registerEventHandlers(aliceEventEmitter); diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index 28bb0d4c443..c210d14c80b 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -231,7 +231,7 @@ describe("MegolmBackup", function () { test("fail if given backup has no version", async () => { const client = makeTestClient(cryptoStore); - await client.initCrypto(); + await client.initLegacyCrypto(); const data = { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: { @@ -314,7 +314,7 @@ describe("MegolmBackup", function () { megolmDecryption.olmlib = mockOlmLib; return client - .initCrypto() + .initLegacyCrypto() .then(() => { return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( @@ -391,7 +391,7 @@ describe("MegolmBackup", function () { megolmDecryption.olmlib = mockOlmLib; return client - .initCrypto() + .initLegacyCrypto() .then(() => { return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32)); }) @@ -471,7 +471,7 @@ describe("MegolmBackup", function () { // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; - await client.initCrypto(); + await client.initLegacyCrypto(); client.uploadDeviceSigningKeys = async function (e) { return {}; }; @@ -560,7 +560,7 @@ describe("MegolmBackup", function () { // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; - await client.initCrypto(); + await client.initLegacyCrypto(); await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", @@ -636,7 +636,7 @@ describe("MegolmBackup", function () { // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; - return client.initCrypto(); + return client.initLegacyCrypto(); }); afterEach(function () { @@ -773,7 +773,7 @@ describe("MegolmBackup", function () { // initialising the crypto library will trigger a key upload request, which we can stub out client.uploadKeysRequest = jest.fn(); - await client.initCrypto(); + await client.initLegacyCrypto(); cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6); await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6); @@ -784,7 +784,7 @@ describe("MegolmBackup", function () { describe("getKeyBackupInfo", () => { it("should return throw an `Not implemented`", async () => { const client = makeTestClient(cryptoStore); - await client.initCrypto(); + await client.initLegacyCrypto(); await expect(client.getCrypto()?.getKeyBackupInfo()).rejects.toThrow("Not implemented"); }); }); diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index 967c86ca69a..a8b7fa2624b 100644 --- a/spec/unit/crypto/cross-signing.spec.ts +++ b/spec/unit/crypto/cross-signing.spec.ts @@ -78,7 +78,7 @@ async function makeTestClient( const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options); const client = testClient.client; - await client.initCrypto(); + await client.initLegacyCrypto(); return { client, httpBackend: testClient.httpBackend }; } diff --git a/spec/unit/crypto/dehydration.spec.ts b/spec/unit/crypto/dehydration.spec.ts index 37893230a46..d9a0dac895e 100644 --- a/spec/unit/crypto/dehydration.spec.ts +++ b/spec/unit/crypto/dehydration.spec.ts @@ -68,7 +68,7 @@ describe("Dehydration", () => { }, }); - await alice.client.initCrypto(); + await alice.client.initLegacyCrypto(); alice.httpBackend.when("GET", "/room_keys/version").respond(404, { errcode: "M_NOT_FOUND", diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index d7c7516a98b..88b011870f0 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -43,7 +43,7 @@ async function makeTestClient( return true; }; - await client.initCrypto(); + await client.initLegacyCrypto(); // No need to download keys for these tests jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map()); diff --git a/spec/unit/crypto/verification/util.ts b/spec/unit/crypto/verification/util.ts index 6454fd6004a..16a18559870 100644 --- a/spec/unit/crypto/verification/util.ts +++ b/spec/unit/crypto/verification/util.ts @@ -119,7 +119,7 @@ export async function makeTestClients( clients.push(testClient); } - await Promise.all(clients.map((testClient) => testClient.client.initCrypto())); + await Promise.all(clients.map((testClient) => testClient.client.initLegacyCrypto())); const destroy = () => { timeouts.forEach((t) => clearTimeout(t)); diff --git a/src/client.ts b/src/client.ts index a13eceebcc4..2de41f2ca23 100644 --- a/src/client.ts +++ b/src/client.ts @@ -294,7 +294,7 @@ export interface ICreateClientOpts { * specified, uses a default implementation (indexeddb in the browser, * in-memory otherwise). * - * This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}), + * This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}), * but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device * previously used legacy crypto (so must be migrated), then this must still be provided, so that the * data can be migrated from the legacy store. @@ -389,7 +389,7 @@ export interface ICreateClientOpts { * * This must be set to the same value every time the client is initialised for the same device. * - * This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}), + * This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}), * but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device * previously used legacy crypto (so must be migrated), then this must still be provided, so that the * data can be migrated from the legacy store. @@ -1222,7 +1222,7 @@ export class MatrixClient extends TypedEventEmitter(this); - public olmVersion: [number, number, number] | null = null; // populated after initCrypto + public olmVersion: [number, number, number] | null = null; // populated after initLegacyCrypto public usingExternalCrypto = false; private _store!: Store; public deviceId: string | null; @@ -1605,7 +1605,7 @@ export class MatrixClient extends TypedEventEmitter { + public async initLegacyCrypto(): Promise { if (!isCryptoAvailable()) { throw new Error( `End-to-end encryption not supported in this js-sdk build: did ` + @@ -2220,7 +2220,7 @@ export class MatrixClient extends TypedEventEmitter Date: Thu, 5 Dec 2024 18:08:41 +0200 Subject: [PATCH 14/55] Fix age field check in event echo processing (#3635) Co-authored-by: David Baker --- src/models/event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/event.ts b/src/models/event.ts index bdb75f5ea2b..9e52e0d9365 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1396,7 +1396,7 @@ export class MatrixEvent extends TypedEventEmitter Date: Thu, 5 Dec 2024 19:21:39 +0000 Subject: [PATCH 15/55] Update dependency typedoc to v0.27.3 (#4573) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2cfe41e0730..412fd1f060d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6213,9 +6213,9 @@ typedoc-plugin-missing-exports@^3.0.0: integrity sha512-Sogbaj+qDa21NjB3SlIw4JXSwmcl/WOjwiPNaVEcPhpNG/MiRTtpwV81cT7h1cbu9StpONFPbddYWR0KV/fTWA== typedoc@^0.27.0: - version "0.27.2" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.2.tgz#8a4e0303f4c49174af21e981e0b60e8a637d8167" - integrity sha512-C2ima5TZJHU3ecnRIz50lKd1BsYck5LhYQIy7MRPmjuSEJreUEAt+uAVcZgY7wZsSORzEI7xW8miZIdxv/cbmw== + version "0.27.3" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.3.tgz#0fad232181ce0ac7eda27fe78e56a4b863e1fe59" + integrity sha512-oWT7zDS5oIaxYL5yOikBX4cL99CpNAZn6mI24JZQxsYuIHbtguSSwJ7zThuzNNwSE0wqhlfTSd99HgqKu2aQXQ== dependencies: "@gerrit0/mini-shiki" "^1.24.0" lunr "^2.3.9" From ded87290ce5db3aa356d76a3298a2423bccc131b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 9 Dec 2024 18:11:02 -0500 Subject: [PATCH 16/55] Update matrix-sdk-crypto-wasm to 11.0.0 (#4566) * Update matrix-sdk-crypto-wasm to 11.0.0 * use `backend` variable to test for rust crypto * apply changes from review --- package.json | 2 +- spec/integ/crypto/verification.spec.ts | 13 ++++++++++ spec/unit/rust-crypto/rust-crypto.spec.ts | 28 ++++++++++++++++++--- src/rust-crypto/rust-crypto.ts | 14 ++++++----- yarn.lock | 30 +++++------------------ 5 files changed, 53 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 381e9ec7d9a..4ed1249f9d6 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index 7b207a432da..a4cee9e8365 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -78,6 +78,7 @@ import { encryptGroupSessionKey, encryptMegolmEvent, encryptSecretSend, + getTestOlmAccountKeys, ToDeviceEvent, } from "./olm-utils"; import { KeyBackupInfo } from "../../../src/crypto-api"; @@ -992,6 +993,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st aliceClient.setGlobalErrorOnUnknownDevices(false); syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID])); await syncPromise(aliceClient); + + // Rust crypto requires the sender's device keys before it accepts a + // verification request. + if (backend === "rust-sdk") { + const crypto = aliceClient.getCrypto()!; + + const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice"); + e2eKeyResponder.addDeviceKeys(bobDeviceKeys); + syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } }); + await syncPromise(aliceClient); + await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]); + } }); /** diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index fc9b571acb2..aa4d9452059 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -572,15 +572,37 @@ describe("RustCrypto", () => { }); it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => { + rustCrypto = await makeTestRustCrypto( + new MatrixHttpApi(new TypedEventEmitter(), { + baseUrl: "http://server/", + prefix: "", + onlyData: true, + }), + testData.TEST_USER_ID, + ); + + fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} }); + fetchMock.post("path:/_matrix/client/v3/keys/query", { + device_keys: { + [testData.TEST_USER_ID]: { + [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA, + }, + }, + }); + + // wait until we know about the other device + rustCrypto.onSyncCompleted({}); + await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]); + const toDeviceEvent = { type: "m.key.verification.request", content: { - from_device: "testDeviceId", + from_device: testData.TEST_DEVICE_ID, methods: ["m.sas.v1"], transaction_id: "testTxn", timestamp: Date.now() - 1000, }, - sender: "@user:id", + sender: testData.TEST_USER_ID, }; const onEvent = jest.fn(); @@ -1015,7 +1037,7 @@ describe("RustCrypto", () => { ["Not encrypted.", RustSdkCryptoJs.ShieldStateCode.SentInClear, EventShieldReason.SENT_IN_CLEAR], [ "Encrypted by a previously-verified user who is no longer verified.", - RustSdkCryptoJs.ShieldStateCode.PreviouslyVerified, + RustSdkCryptoJs.ShieldStateCode.VerificationViolation, EventShieldReason.VERIFICATION_VIOLATION, ], ])("gets the right shield reason (%s)", async (rustReason, rustCode, expectedReason) => { diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 86853a9867a..d5a8736f551 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -653,7 +653,7 @@ export class RustCrypto extends TypedEventEmitter { - const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined = + const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined = await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId)); if (userIdentity === undefined) { return new UserVerificationStatus(false, false, false); @@ -662,7 +662,9 @@ export class RustCrypto extends TypedEventEmitter { - const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined = + const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined = await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId)); if (userIdentity === undefined) { @@ -1020,7 +1022,7 @@ export class RustCrypto extends TypedEventEmitter { - const userIdentity: RustSdkCryptoJs.UserIdentity | undefined = await this.olmMachine.getIdentity( + const userIdentity: RustSdkCryptoJs.OtherUserIdentity | undefined = await this.olmMachine.getIdentity( new RustSdkCryptoJs.UserId(userId), ); @@ -2035,7 +2037,7 @@ class EventDecryptor { errorDetails, ); - case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityPreviouslyVerified: + case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityVerificationViolation: // We're refusing to decrypt due to not trusting the sender, // rather than failing to decrypt due to lack of keys, so we // don't need to keep it on the pending list. @@ -2200,7 +2202,7 @@ function rustEncryptionInfoToJsEncryptionInfo( case RustSdkCryptoJs.ShieldStateCode.SentInClear: shieldReason = EventShieldReason.SENT_IN_CLEAR; break; - case RustSdkCryptoJs.ShieldStateCode.PreviouslyVerified: + case RustSdkCryptoJs.ShieldStateCode.VerificationViolation: shieldReason = EventShieldReason.VERIFICATION_VIOLATION; break; } diff --git a/yarn.lock b/yarn.lock index 412fd1f060d..a7ea2491c3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,10 +1477,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz#f889653eb4fafaad2a963654d586bd34de62acd5" - integrity sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og== +"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" + integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -5846,16 +5846,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6454,16 +6445,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 3de2b9bf800164d9e0f3457b7e0da5102ad2b13e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 10 Dec 2024 15:43:43 +0000 Subject: [PATCH 17/55] v35.0.0-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ed1249f9d6..bebe0ab3a0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "34.13.0", + "version": "35.0.0-rc.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=20.0.0" From 5998de365d74382d6d8311983c25364d87ffc06e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:31:44 +0000 Subject: [PATCH 18/55] Update dependency @babel/cli to v7.26.4 (#4580) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index a7ea2491c3b..eb8a779376e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,9 +23,9 @@ "@jridgewell/trace-mapping" "^0.3.24" "@babel/cli@^7.12.10": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.25.9.tgz#51036166fd0e9cfb26eee1b9ddc264a0d6d5f843" - integrity sha512-I+02IfrTiSanpxJBlZQYb18qCxB6c2Ih371cVpfgIrPQrjAYkf45XxomTJOG8JBWX5GY35/+TmhCMdJ4ZPkL8Q== + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.26.4.tgz#4101ff8ee5de8447a6c395397a97921056411d20" + integrity sha512-+mORf3ezU3p3qr+82WvJSnQNE1GAYeoCfEv4fik6B5/2cvKZ75AX8oawWQdXtM9MmndooQj15Jr9kelRFWsuRw== dependencies: "@jridgewell/trace-mapping" "^0.3.25" commander "^6.2.0" @@ -5846,7 +5846,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6445,7 +6454,16 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From e78a3cec9ff41f0977925ee7811ad834d456dbcd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:50:03 +0000 Subject: [PATCH 19/55] Update all non-major dependencies (#4579) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 69 ++++++++++++++++++++++++++++------------------------ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 4ed1249f9d6..8be005eb3d6 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "lint-staged": "^15.0.2", "matrix-mock-request": "^2.5.0", "node-fetch": "^2.7.0", - "prettier": "3.4.1", + "prettier": "3.4.2", "rimraf": "^6.0.0", "ts-node": "^10.9.2", "typedoc": "^0.27.0", diff --git a/yarn.lock b/yarn.lock index eb8a779376e..f6a197b2714 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2701,7 +2701,7 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@~4.3.6: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.6, debug@~4.3.6: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -2715,6 +2715,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.4, debug@^4.3.7: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -3032,18 +3039,18 @@ eslint-import-resolver-node@^0.3.9: resolve "^1.22.4" eslint-import-resolver-typescript@^3.5.1: - version "3.6.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e" - integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA== + version "3.7.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz#e69925936a771a9cb2de418ccebc4cdf6c0818aa" + integrity sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow== dependencies: "@nolyfill/is-core-module" "1.0.39" - debug "^4.3.5" + debug "^4.3.7" enhanced-resolve "^5.15.0" - eslint-module-utils "^2.8.1" fast-glob "^3.3.2" get-tsconfig "^4.7.5" is-bun-module "^1.0.2" is-glob "^4.0.3" + stable-hash "^0.0.4" eslint-module-utils@^2.12.0: version "2.12.0" @@ -3052,13 +3059,6 @@ eslint-module-utils@^2.12.0: dependencies: debug "^3.2.7" -eslint-module-utils@^2.8.1: - version "2.9.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342" - integrity sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ== - dependencies: - debug "^3.2.7" - eslint-plugin-es@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9" @@ -3615,9 +3615,9 @@ get-symbol-description@^1.0.2: get-intrinsic "^1.2.4" get-tsconfig@^4.7.5: - version "4.8.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.0.tgz#125dc13a316f61650a12b20c97c11b8fd996fedd" - integrity sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.1.tgz#8995eb391ae6e1638d251118c7b56de7eb425471" + integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== dependencies: resolve-pkg-maps "^1.0.0" @@ -3904,9 +3904,9 @@ is-builtin-module@^3.2.1: builtin-modules "^3.3.0" is-bun-module@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.1.0.tgz#a66b9830869437f6cdad440ba49ab6e4dc837269" - integrity sha512-4mTAVPlrXpaN3jtF0lsnPCMGnq4+qZjVIKq0HCpfcqf8OC1SM5oATCIAPM5V5FN05qp2NNnFndphmdZS9CV3hA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.3.0.tgz#ea4d24fdebfcecc98e81bcbcb506827fee288760" + integrity sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA== dependencies: semver "^7.6.3" @@ -4557,9 +4557,9 @@ jest@^29.0.0: jest-cli "^29.7.0" jiti@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.0.tgz#393d595fb6031a11d11171b5e4fc0b989ba3e053" - integrity sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g== + version "2.4.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.1.tgz#4de9766ccbfa941d9b6390d2b159a4b295a52e6b" + integrity sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g== jju@~1.4.0: version "1.4.0" @@ -4693,9 +4693,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.0.0: - version "5.38.1" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.38.1.tgz#ff5a510fe43841a21a84748b3ed1d50069b2d814" - integrity sha512-qGQpVO9jhHDoJ/4O1paXQ8Y6XyqH3Xm6OTety/z5IouZBEvJuJoWp59iY9E82Dt0pz9BBmKLczliB4sbYMPr2g== + version "5.39.2" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.39.2.tgz#1faacd8d8ef36b509b2f6e396cce85b645abb04e" + integrity sha512-BuvuWRllLWV/r2G4m9ggNH+DZ6gouP/dhtJPXVlMbWNF++w9/EfrF6k2g7YBKCwjzCC+PXmYtpH8S2t8RjnY4Q== dependencies: "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" @@ -5325,10 +5325,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.1.tgz#e211d451d6452db0a291672ca9154bc8c2579f7b" - integrity sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg== +prettier@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== pretty-format@^28.1.3: version "28.1.3" @@ -5826,6 +5826,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +stable-hash@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.4.tgz#55ae7dadc13e4b3faed13601587cec41859b42f7" + integrity sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g== + stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -6577,6 +6582,6 @@ zod-validation-error@^3.0.3: integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ== zod@^3.22.4: - version "3.23.8" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" - integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + version "3.24.0" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.0.tgz#babb32313f7c5f4a99812feee806d186b4f76bde" + integrity sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w== From 413c15662486ea2615f78fd9c7ced9ae14ad865b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:12:27 +0000 Subject: [PATCH 20/55] Update guibranco/github-status-action-v2 digest to d469d49 (#4576) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/sonarcloud.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 682c9257341..aea6aec5665 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -27,7 +27,7 @@ jobs: steps: # We create the status here and then update it to success/failure in the `report` stage # This provides an easy link to this workflow_run from the PR before Sonarcloud is done. - - uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c + - uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: pending @@ -87,7 +87,7 @@ jobs: revision: ${{ github.event.workflow_run.head_sha }} token: ${{ secrets.SONAR_TOKEN }} - - uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c + - uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 if: always() with: authToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc4d19a2a14..29393c9352f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -116,7 +116,7 @@ jobs: steps: - name: Skip SonarCloud on merge queues if: env.ENABLE_COVERAGE == 'false' - uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c + uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success From 8155b0acfcfdb7f1080dca0e8876783892de34ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:11:53 +0000 Subject: [PATCH 21/55] Update mheap/github-action-required-labels digest to 388fd6a (#4577) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pull_request.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 624b8f2fd35..98572ff6a11 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -15,7 +15,7 @@ jobs: name: Preview Changelog runs-on: ubuntu-24.04 steps: - - uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5 + - uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5 if: github.event_name != 'merge_group' with: labels: | From 3ae25427a80e4d6bdd5cfa043df2c5565de2ef0b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:53:08 +0000 Subject: [PATCH 22/55] Update dependency @types/node to v18.19.67 (#4578) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f6a197b2714..7c08cc82ad0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1803,9 +1803,9 @@ undici-types "~5.26.4" "@types/node@18": - version "18.19.66" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.66.tgz#0937a47904ceba5994eedf5cf4b6d503d8d6136c" - integrity sha512-14HmtUdGxFUalGRfLLn9Gc1oNWvWh5zNbsyOLo5JV6WARSeN1QcEBKRnZm9QqNfrutgsl/hY4eJW63aZ44aBCg== + version "18.19.67" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.67.tgz#77c4b01641a1e3e1509aff7e10d39e4afd5ae06d" + integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== dependencies: undici-types "~5.26.4" From d1de32ea2773df4c6f8a956678bbd19b6d022475 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:58:50 +0100 Subject: [PATCH 23/55] Only re-prepare MatrixrRTC delayed disconnection event on 404 (#4575) * Set retry counts of event updating to 1000 (from 1) With it being set to one the following issue could occur: ``` // If sending state cancels your own delayed state, prepare another delayed state // TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state if (this.disconnectDelayId !== undefined) { try { const knownDisconnectDelayId = this.disconnectDelayId; await resendIfRateLimited( () => this.client._unstable_updateDelayedEvent( knownDisconnectDelayId, UpdateDelayedEventAction.Restart, ), 1000, ); } catch (e) { logger.warn("Failed to update delayed disconnection event, prepare it again:", e); this.disconnectDelayId = undefined; await prepareDelayedDisconnection(); } } ``` This code looks like the `catch(e)` could never be triggered with 429 (rate limit) because they would be caught by `await resendIfRateLimited`. EXCEPT that this is only happening once: `resendIfRateLimited(func: () => Promise, numRetriesAllowed: number = 1)`. So as soon as the server sends two rate limits in a row we get the following: - we get into the `catch(e)` because of the rate limit - we forget about `this.disconnectDelayId = undefined` - we start a new delayed event `await prepareDelayedDisconnection();` - we do not anymore update the old delayed event which is still running! - the running delay event will make us disconnect from the call (call member becomes `{}`) - we get into our outher error catching mechanism that resends the new state event - this cancels the newly created delay leave event (`await prepareDelayedDisconnection();`) - and create another delay leave event. - but if we are still reate limited (chances are really high due to the reconnect), this loop will REPEAT * also check for M_NOT_FOUND * Leave retry at current level --------- Co-authored-by: Hugh Nimmo-Smith --- src/matrixrtc/MatrixRTCSession.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 147a6e0a05e..855596b7976 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -1189,9 +1189,14 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 12 Dec 2024 15:03:19 +0000 Subject: [PATCH 24/55] Save the key backup key to 4S during `bootstrapCrossSigning` (#4542) * Save the key backup key to secret storage When setting up secret storage, if we have a key backup key in cache (like we do for the cross signing secrets). * Add test * Get the key directly from the olmMachine saves converting it needlessly into a buffer to turn it back into a base64 string * Overwrite backup keyin storage if different * Fix test * Add integ test * Test failure case for sonar * Unused import * Missed return * Also check active backup version --- spec/integ/crypto/crypto.spec.ts | 26 +++++ spec/unit/rust-crypto/rust-crypto.spec.ts | 113 ++++++++++++++++++++++ src/rust-crypto/rust-crypto.ts | 44 ++++++++- 3 files changed, 182 insertions(+), 1 deletion(-) diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 96d809aea99..386f3dd531c 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -3121,6 +3121,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!; expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined(); }); + + newBackendOnly("should upload existing megolm backup key to a new 4S store", async () => { + const backupKeyTo4SPromise = awaitMegolmBackupKeyUpload(); + + // we need these to set up the mocks but we don't actually care whether they + // resolve because we're not testing those things in this test. + awaitCrossSigningKeyUpload("master"); + awaitCrossSigningKeyUpload("user_signing"); + awaitCrossSigningKeyUpload("self_signing"); + awaitSecretStorageKeyStoredInAccountData(); + + mockSetupCrossSigningRequests(); + mockSetupMegolmBackupRequests("1"); + + await aliceClient.getCrypto()!.bootstrapCrossSigning({}); + await aliceClient.getCrypto()!.resetKeyBackup(); + + await aliceClient.getCrypto()!.bootstrapSecretStorage({ + setupNewSecretStorage: true, + createSecretStorageKey, + setupNewKeyBackup: false, + }); + + await backupKeyTo4SPromise; + expect(accountDataAccumulator.accountDataEvents.get("m.megolm_backup.v1")).toBeDefined(); + }); }); describe("Manage Key Backup", () => { diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index aa4d9452059..8e177097bfe 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -727,6 +727,119 @@ describe("RustCrypto", () => { expect(resetKeyBackup.mock.calls).toHaveLength(2); }); + describe("upload existing key backup key to new 4S store", () => { + const secretStorageCallbacks = { + getSecretStorageKey: async (keys: any, name: string) => { + return [[...Object.keys(keys.keys)][0], new Uint8Array(32)]; + }, + } as SecretStorageCallbacks; + let secretStorage: ServerSideSecretStorageImpl; + + let backupAuthData: any; + let backupAlg: string; + + const fetchMock = { + authedRequest: jest.fn().mockImplementation((method, path, query, body) => { + if (path === "/room_keys/version") { + if (method === "POST") { + backupAuthData = body["auth_data"]; + backupAlg = body["algorithm"]; + return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData }); + } else if (method === "GET" && backupAuthData) { + return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData }); + } + } + return Promise.resolve({}); + }), + }; + + beforeEach(() => { + backupAuthData = undefined; + backupAlg = ""; + + secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks); + }); + + it("bootstrapSecretStorage saves megolm backup key if already cached", async () => { + const rustCrypto = await makeTestRustCrypto( + fetchMock as unknown as MatrixHttpApi, + testData.TEST_USER_ID, + undefined, + secretStorage, + ); + + async function createSecretStorageKey() { + return { + keyInfo: {} as AddSecretStorageKeyOpts, + privateKey: new Uint8Array(32), + }; + } + + await rustCrypto.resetKeyBackup(); + + const storeSpy = jest.spyOn(secretStorage, "store"); + + await rustCrypto.bootstrapSecretStorage({ + createSecretStorageKey, + setupNewSecretStorage: true, + setupNewKeyBackup: false, + }); + + expect(storeSpy).toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything()); + }); + + it("bootstrapSecretStorage doesn't try to save megolm backup key not in cache", async () => { + const mockOlmMachine = { + isBackupEnabled: jest.fn().mockResolvedValue(false), + sign: jest.fn().mockResolvedValue({ + asJSON: jest.fn().mockReturnValue("{}"), + }), + saveBackupDecryptionKey: jest.fn(), + crossSigningStatus: jest.fn().mockResolvedValue({ + hasMaster: true, + hasSelfSigning: true, + hasUserSigning: true, + }), + exportCrossSigningKeys: jest.fn().mockResolvedValue({ + masterKey: "sosecret", + userSigningKey: "secrets", + self_signing_key: "ssshhh", + }), + getBackupKeys: jest.fn().mockResolvedValue({}), + verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }), + } as unknown as OlmMachine; + + const rustCrypto = new RustCrypto( + logger, + mockOlmMachine, + fetchMock as unknown as MatrixHttpApi, + TEST_USER, + TEST_DEVICE_ID, + secretStorage, + {} as CryptoCallbacks, + ); + + async function createSecretStorageKey() { + return { + keyInfo: {} as AddSecretStorageKeyOpts, + privateKey: new Uint8Array(32), + }; + } + + await rustCrypto.resetKeyBackup(); + + const storeSpy = jest.spyOn(secretStorage, "store"); + + await rustCrypto.bootstrapSecretStorage({ + createSecretStorageKey, + setupNewSecretStorage: true, + setupNewKeyBackup: false, + }); + + expect(storeSpy).not.toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything()); + }); + }); + it("isSecretStorageReady", async () => { const mockSecretStorage = { getDefaultKeyId: jest.fn().mockResolvedValue(null), diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index d5a8736f551..ad9e30b292d 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -843,11 +843,53 @@ export class RustCrypto extends TypedEventEmitter { + const keyBackupInfo = await this.backupManager.getServerBackupInfo(); + if (!keyBackupInfo || !keyBackupInfo.version) { + logger.info("Not saving backup key to secret storage: no backup info"); + return; + } + + const activeBackupVersion = await this.backupManager.getActiveBackupVersion(); + if (!activeBackupVersion || activeBackupVersion !== keyBackupInfo.version) { + logger.info("Not saving backup key to secret storage: backup keys do not match active backup version"); + return; + } + + const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys(); + if (!backupKeys.decryptionKey) { + logger.info("Not saving backup key to secret storage: no backup key"); + return; + } + + if (!decryptionKeyMatchesKeyBackupInfo(backupKeys.decryptionKey, keyBackupInfo)) { + logger.info("Not saving backup key to secret storage: decryption key does not match backup info"); + return; + } + + const backupKeyFromStorage = await this.secretStorage.get("m.megolm_backup.v1"); + const backupKeyBase64 = backupKeys.decryptionKey.toBase64(); + + // The backup version that the key corresponds to isn't saved in 4S so if it's different, we must assume + // it's stale and overwrite. + if (backupKeyFromStorage !== backupKeyBase64) { + await this.secretStorage.store("m.megolm_backup.v1", backupKeyBase64); + } + } + /** * Add the secretStorage key to the secret storage * - The secret storage key must have the `keyInfo` field filled From 315e81b7decd67154eaedfbaf4cf3fbecbb99e7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:45:39 +0000 Subject: [PATCH 25/55] Update typescript-eslint monorepo to v8.17.0 (#4581) * Update typescript-eslint monorepo to v8.17.0 * Fix lint errors --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: David Baker --- spec/integ/crypto/crypto.spec.ts | 4 +- spec/integ/crypto/megolm-backup.spec.ts | 2 +- spec/test-utils/mockEndpoints.ts | 2 +- src/client.ts | 2 +- .../store/indexeddb-crypto-store-backend.ts | 2 +- src/store/indexeddb-local-backend.ts | 4 +- yarn.lock | 105 +++++++++++++----- 7 files changed, 83 insertions(+), 38 deletions(-) diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 386f3dd531c..d3f5e20f719 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -3168,7 +3168,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, fetchMock.put( "path:/_matrix/client/v3/room_keys/keys", (url, request) => { - const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}"); + const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}"); resolve(uploadPayload); return { status: 200, @@ -3235,7 +3235,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, fetchMock.post( "path:/_matrix/client/v3/room_keys/version", (url, request) => { - const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}"); + const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}"); backupData.version = newVersion; backupData.count = 0; backupData.etag = "zer"; diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 05b5ff27b60..eff0ff567e1 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -91,7 +91,7 @@ function mockUploadEmitter( }, }; } - const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}"); + const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}"); let count = 0; for (const [roomId, value] of Object.entries(uploadPayload.rooms)) { for (const sessionId of Object.keys(value.sessions)) { diff --git a/spec/test-utils/mockEndpoints.ts b/spec/test-utils/mockEndpoints.ts index 988d6f13b6c..7c16583885e 100644 --- a/spec/test-utils/mockEndpoints.ts +++ b/spec/test-utils/mockEndpoints.ts @@ -88,7 +88,7 @@ export function mockSetupMegolmBackupRequests(backupVersion: string): void { }); fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => { - const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}"); + const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}"); backupData.version = backupVersion; backupData.count = 0; backupData.etag = "zer"; diff --git a/src/client.ts b/src/client.ts index 2de41f2ca23..54f53094282 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5065,7 +5065,7 @@ export class MatrixClient extends TypedEventEmitter( return new Promise((resolve, reject) => { const results: T[] = []; query.onerror = (): void => { - reject(new Error("Query failed: " + query.error)); + reject(new Error("Query failed: " + query.error?.name)); }; // collect results query.onsuccess = (): void => { @@ -360,7 +360,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { // in firefox, with indexedDB disabled, this fails with a // DOMError. We treat this as non-fatal, so that we can still // use the app. - logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`); + logger.warn(`unable to delete js-sdk store indexeddb: ${req.error?.name}`); resolve(); }; diff --git a/yarn.lock b/yarn.lock index 7c08cc82ad0..21e63feca39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1857,29 +1857,29 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz#ac56825bcdf3b392fc76a94b1315d4a162f201a6" - integrity sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q== + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz#0901933326aea4443b81df3f740ca7dfc45c7bea" + integrity sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/type-utils" "8.16.0" - "@typescript-eslint/utils" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/type-utils" "8.18.0" + "@typescript-eslint/utils" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^8.0.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.16.0.tgz#ee5b2d6241c1ab3e2e53f03fd5a32d8e266d8e06" - integrity sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w== + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.0.tgz#a1c9456cbb6a089730bf1d3fc47946c5fb5fe67b" + integrity sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q== dependencies: - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/typescript-estree" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/typescript-estree" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" debug "^4.3.4" "@typescript-eslint/scope-manager@8.14.0": @@ -1898,13 +1898,21 @@ "@typescript-eslint/types" "8.16.0" "@typescript-eslint/visitor-keys" "8.16.0" -"@typescript-eslint/type-utils@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz#585388735f7ac390f07c885845c3d185d1b64740" - integrity sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg== +"@typescript-eslint/scope-manager@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz#30b040cb4557804a7e2bcc65cf8fdb630c96546f" + integrity sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw== dependencies: - "@typescript-eslint/typescript-estree" "8.16.0" - "@typescript-eslint/utils" "8.16.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" + +"@typescript-eslint/type-utils@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz#6f0d12cf923b6fd95ae4d877708c0adaad93c471" + integrity sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow== + dependencies: + "@typescript-eslint/typescript-estree" "8.18.0" + "@typescript-eslint/utils" "8.18.0" debug "^4.3.4" ts-api-utils "^1.3.0" @@ -1918,6 +1926,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737" integrity sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ== +"@typescript-eslint/types@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3" + integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA== + "@typescript-eslint/typescript-estree@8.14.0": version "8.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312" @@ -1946,15 +1959,29 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.16.0", "@typescript-eslint/utils@^8.13.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" - integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== +"@typescript-eslint/typescript-estree@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz#d8ca785799fbb9c700cdff1a79c046c3e633c7f9" + integrity sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg== + dependencies: + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.0.tgz#48f67205d42b65d895797bb7349d1be5c39a62f7" + integrity sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/typescript-estree" "8.16.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/typescript-estree" "8.18.0" "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": version "8.14.0" @@ -1966,6 +1993,16 @@ "@typescript-eslint/types" "8.14.0" "@typescript-eslint/typescript-estree" "8.14.0" +"@typescript-eslint/utils@^8.13.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" + integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.16.0" + "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/typescript-estree" "8.16.0" + "@typescript-eslint/visitor-keys@8.14.0": version "8.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz#2418d5a54669af9658986ade4e6cfb7767d815ad" @@ -1982,6 +2019,14 @@ "@typescript-eslint/types" "8.16.0" eslint-visitor-keys "^4.2.0" +"@typescript-eslint/visitor-keys@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz#7b6d33534fa808e33a19951907231ad2ea5c36dd" + integrity sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw== + dependencies: + "@typescript-eslint/types" "8.18.0" + eslint-visitor-keys "^4.2.0" + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -6083,9 +6128,9 @@ tr46@~0.0.3: integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== ts-api-utils@^1.3.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.2.tgz#a6a6dff26117ac7965624fc118525971edc6a82a" - integrity sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw== + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== ts-node@^10.9.2: version "10.9.2" From 693bb22ba1478336afbffe63d492214eaaa9cd6c Mon Sep 17 00:00:00 2001 From: Liam Diprose Date: Tue, 17 Dec 2024 00:38:34 +1300 Subject: [PATCH 26/55] Handle when aud OIDC claim is an Array (#4584) * Handle when `aud` OIDC claim is an Array The `aud` claim of OIDC id_tokens [can be an array](https://github.com/authts/oidc-client-ts/blob/ce6d694639c58e6a1c80904efdac5eda82b82042/src/Claims.ts#L92) but the existing logic incorrectly assumes `aud` is always a string. This PR adds the necessary check. * Clarify `aud` OIDC claim check * Fix for prettier --------- Co-authored-by: David Baker --- spec/unit/oidc/validate.spec.ts | 17 +++++++++++++++++ src/oidc/validate.ts | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index c9207e28fa4..bfb40a15b5f 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -170,6 +170,23 @@ describe("validateIdToken()", () => { expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience")); }); + it("should not throw when audience is an array that includes clientId", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + aud: [clientId], + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow(); + }); + + it("should throw when audience is an array that does not include clientId", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + aud: [`${clientId},uiop`, "asdf"], + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience")); + }); + it("should throw when nonce does not match", () => { mocked(jwtDecode).mockReturnValue({ ...validDecodedIdToken, diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 72eb7e96e64..ce62e90eb6c 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -179,7 +179,8 @@ export const validateIdToken = ( * The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. * EW: Don't accept tokens with other untrusted audiences * */ - if (claims.aud !== clientId) { + const sanitisedAuds = typeof claims.aud === "string" ? [claims.aud] : claims.aud; + if (!sanitisedAuds.includes(clientId)) { throw new Error("Invalid audience"); } From aba4e690afe72433ebc7f0ee0bf4ae26ba1eea66 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:00:18 +0000 Subject: [PATCH 27/55] Improve documentation on various secret-storage related methods (#4585) * Improve documentation on various secret-storage related methods * fix link * Apply suggestions from code review --- src/crypto-api/index.ts | 106 ++++++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 20 deletions(-) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 94c4ac5a5c2..4a780696770 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -20,7 +20,7 @@ import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.t import { Room } from "../models/room.ts"; import { DeviceMap } from "../models/device.ts"; import { UIAuthCallback } from "../interactive-auth.ts"; -import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "../secret-storage.ts"; +import { PassphraseInfo, SecretStorageKeyDescription } from "../secret-storage.ts"; import { VerificationRequest } from "./verification.ts"; import { BackupTrustInfo, @@ -268,9 +268,9 @@ export interface CryptoApi { * - is enabled on this account and trusted by this device * - has private keys either cached locally or stored in secret storage * - * If this function returns false, bootstrapCrossSigning() can be used + * If this function returns false, {@link bootstrapCrossSigning()} can be used * to fix things such that it returns true. That is to say, after - * bootstrapCrossSigning() completes successfully, this function should + * `bootstrapCrossSigning()` completes successfully, this function should * return true. * * @returns True if cross-signing is ready to be used on this device @@ -317,9 +317,9 @@ export interface CryptoApi { * - is storing cross-signing private keys * - is storing session backup key (if enabled) * - * If this function returns false, bootstrapSecretStorage() can be used + * If this function returns false, {@link bootstrapSecretStorage()} can be used * to fix things such that it returns true. That is to say, after - * bootstrapSecretStorage() completes successfully, this function should + * `bootstrapSecretStorage()` completes successfully, this function should * return true. * * @returns True if secret storage is ready to be used on this device @@ -327,17 +327,20 @@ export interface CryptoApi { isSecretStorageReady(): Promise; /** - * Bootstrap the secret storage by creating a new secret storage key, add it in the secret storage and - * store the cross signing keys in the secret storage. + * Bootstrap [secret storage](https://spec.matrix.org/v1.12/client-server-api/#storage). * - * - Generate a new key {@link GeneratedSecretStorageKey} with `createSecretStorageKey`. - * Only if `setupNewSecretStorage` is set or if there is no AES key in the secret storage - * - Store this key in the secret storage and set it as the default key. - * - Call `cryptoCallbacks.cacheSecretStorageKey` if provided. - * - Store the cross signing keys in the secret storage if - * - the cross signing is ready - * - a new key was created during the previous step - * - or the secret storage already contains the cross signing keys + * - If secret storage is not already set up, or {@link CreateSecretStorageOpts.setupNewSecretStorage} is set: + * * Calls {@link CreateSecretStorageOpts.createSecretStorageKey} to generate a new key. + * * Stores the metadata of the new key in account data and sets it as the default secret storage key. + * * Calls {@link CryptoCallbacks.cacheSecretStorageKey} if provided. + * - Stores the private cross signing keys in the secret storage if they are known, and they are not + * already stored in secret storage. + * - If {@link CreateSecretStorageOpts.setupNewKeyBackup} is set, calls {@link CryptoApi.resetKeyBackup}; otherwise, + * stores the key backup decryption key in secret storage if it is known, and it is not + * already stored in secret storage. + * + * Note that there may be multiple accesses to secret storage during the course of this call, each of which will + * result in a call to {@link CryptoCallbacks.getSecretStorageKey}. * * @param opts - Options object. */ @@ -562,9 +565,10 @@ export interface CryptoApi { * * If there are existing backups they will be replaced. * - * The decryption key will be saved in Secret Storage (the {@link matrix.SecretStorage.SecretStorageCallbacks.getSecretStorageKey} Crypto - * callback will be called) - * and the backup engine will be started. + * If secret storage is set up, the new decryption key will be saved (the {@link CryptoCallbacks.getSecretStorageKey} + * callback will be called to obtain the secret storage key). + * + * The backup engine will be started using the new backup version (i.e., {@link checkKeyBackupAndEnable} is called). */ resetKeyBackup(): Promise; @@ -995,15 +999,77 @@ export interface CrossSigningStatus { /** * Crypto callbacks provided by the application */ -export interface CryptoCallbacks extends SecretStorageCallbacks { +export interface CryptoCallbacks { + /** + * Called to retrieve a secret storage encryption key. + * + * [Server-side secret storage](https://spec.matrix.org/v1.12/client-server-api/#key-storage) + * is, as the name implies, a mechanism for storing secrets which should be shared between + * clients on the server. For example, it is typically used for storing the + * [key backup decryption key](https://spec.matrix.org/v1.12/client-server-api/#decryption-key) + * and the private [cross-signing keys](https://spec.matrix.org/v1.12/client-server-api/#cross-signing). + * + * The secret storage mechanism encrypts the secrets before uploading them to the server using a + * secret storage key. The schema supports multiple keys, but in practice only one tends to be used + * at once; this is the "default secret storage key" and may be known as the "recovery key" (or, sometimes, + * the "security key"). + * + * Secret storage can be set up by calling {@link CryptoApi.bootstrapSecretStorage}. Having done so, when + * the crypto stack needs to access secret storage (for example, when setting up a new device, or to + * store newly-generated secrets), it will use this callback (`getSecretStorageKey`). + * + * Note that the secret storage key may be needed several times in quick succession: it is recommended + * that applications use a temporary cache to avoid prompting the user multiple times for the key. See + * also {@link cacheSecretStorageKey} which is called when a new key is created. + * + * The helper method {@link deriveRecoveryKeyFromPassphrase} may be useful if the secret storage key + * was derived from a passphrase. + * + * @param opts - An options object. + * + * @param name - the name of the *secret* (NB: not the encryption key) being stored or retrieved. + * When the item is stored in account data, it will have this `type`. + * + * @returns a pair [`keyId`, `privateKey`], where `keyId` is one of the keys from the `keys` parameter, + * and `privateKey` is the raw private encryption key, as appropriate for the encryption algorithm. + * (For `m.secret_storage.v1.aes-hmac-sha2`, it is the input to an HKDF as defined in the + * [specification](https://spec.matrix.org/v1.6/client-server-api/#msecret_storagev1aes-hmac-sha2).) + * + * Alternatively, if none of the keys are known, may return `null` — in which case the original + * operation that requires access to a secret in secret storage may fail with an exception. + */ + getSecretStorageKey?: ( + opts: { + /** + * Details of the secret storage keys required: a map from the key ID + * (excluding the `m.secret_storage.key.` prefix) to details of the key. + * + * When storing a secret, `keys` will contain exactly one entry. + * + * For secret retrieval, `keys` may contain several entries, and the application can return + * any one of the requested keys. Unless your application specifically wants to offer the + * user the ability to have more than one secret storage key active at a time, it is recommended + * to call {@link matrix.SecretStorage.ServerSideSecretStorage.getDefaultKeyId | ServerSideSecretStorage.getDefaultKeyId} + * to figure out which is the current default key, and to return `null` if the default key is not listed in `keys`. + */ + keys: Record; + }, + name: string, + ) => Promise<[string, Uint8Array] | null>; + /** @deprecated: unused with the Rust crypto stack. */ getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; /** @deprecated: unused with the Rust crypto stack. */ saveCrossSigningKeys?: (keys: Record) => void; /** @deprecated: unused with the Rust crypto stack. */ shouldUpgradeDeviceVerifications?: (users: Record) => Promise; + /** - * Called by {@link CryptoApi#bootstrapSecretStorage} + * Called by {@link CryptoApi.bootstrapSecretStorage} when a new default secret storage key is created. + * + * Applications can use this to (temporarily) cache the secret storage key, for later return by + * {@link getSecretStorageKey}. + * * @param keyId - secret storage key id * @param keyInfo - secret storage key info * @param key - private key to store From 3219aefc9254b162a5a56a6cab0494e29e2e6cc1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Dec 2024 09:22:31 +0000 Subject: [PATCH 28/55] Avoid key prompts when resetting crypto (#4586) * Avoid key prompts when resetting crypto Attempting to get the backup key out of secret storage can cause the user to be prompted for their key, which is not helpful if this is being done as part of a reset. This check was redundant anyway and we can just overwrite the key with the same value. Also fix docs and remove check for active backup. * Fix doc --- src/rust-crypto/rust-crypto.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index ad9e30b292d..e586510440e 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -854,7 +854,7 @@ export class RustCrypto extends TypedEventEmitter { const keyBackupInfo = await this.backupManager.getServerBackupInfo(); @@ -863,12 +863,6 @@ export class RustCrypto extends TypedEventEmitter Date: Tue, 17 Dec 2024 11:24:46 +0000 Subject: [PATCH 29/55] Update matrix-sdk-crypto-wasm to 12.0.0 (#4589) It appears to "just work"... but I might be missing something --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8be005eb3d6..73479e4b1af 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "^12.0.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 21e63feca39..a7cb2097ccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,10 +1477,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" - integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== +"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a" + integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q== "@matrix-org/olm@3.2.15": version "3.2.15" From bee65ff13f1796a540e493ec00e513949e0f366f Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Dec 2024 13:22:10 +0000 Subject: [PATCH 30/55] v35.0.0 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2687d1bdd3..344ebbe7c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +Changes in [35.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.0.0) (2024-12-17) +================================================================================================== +## 🚨 BREAKING CHANGES + +This release contains several breaking changes which will need code changes in your app. Most notably, `initCrypto()` +no longer exists and has been moved to `initLegacyCrypto()` in preparation for the eventual removal of Olm. You can +continue to use legacy Olm crypto for now by calling `initLegacyCrypto()` instead. + +You may also need to make further changes if you use more advanced APIs. See the individual PRs (listed in order of size of change) for specific APIs changed and how to migrate. + +* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros. +* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr. +* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy. + +## 🦖 Deprecations + +* Deprecate remaining legacy functions and move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler ([#4560](https://github.com/matrix-org/matrix-js-sdk/pull/4560)). Contributed by @florianduros. + +## ✨ Features + +* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros. +* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy. +* Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress ([#4561](https://github.com/matrix-org/matrix-js-sdk/pull/4561)). Contributed by @hughns. +* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr. +* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" ([#4532](https://github.com/matrix-org/matrix-js-sdk/pull/4532)). Contributed by @toger5. + +## 🐛 Bug Fixes + +* Fix age field check in event echo processing ([#3635](https://github.com/matrix-org/matrix-js-sdk/pull/3635)). Contributed by @stas-demydiuk. + + Changes in [34.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.13.0) (2024-12-03) ==================================================================================================== ## 🦖 Deprecations diff --git a/package.json b/package.json index bebe0ab3a0e..58506a056e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "35.0.0-rc.0", + "version": "35.0.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=20.0.0" From d33350ff2619fd6c0cbcc9483976281f7fac0ea7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Dec 2024 13:28:11 +0000 Subject: [PATCH 31/55] Upgrade matrix-sdk-crypto-wasm to 1.11.0 ... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 58506a056e6..80b5846c029 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "^11.1.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index a7ea2491c3b..a854b1fa8ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,10 +1477,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" - integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== +"@matrix-org/matrix-sdk-crypto-wasm@^11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.1.0.tgz#289ac0b13961f51329bbecaf6bf14145ab349967" + integrity sha512-JPuO9RCVDklDjbFzMvZfQb7PuiFkLY72bniRSu81lRzkkrcbZtmKqBFMm9H4f2FSz+tHVkDnmsvn12I4sdJJ5A== "@matrix-org/olm@3.2.15": version "3.2.15" From 1bf8533c033c5b462168da2c7f0e25681e9fa67a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 18 Dec 2024 14:10:31 +0000 Subject: [PATCH 32/55] v35.1.0 --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 344ebbe7c49..fedadb80fe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18) +================================================================================================== +This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage. + +## 🐛 Bug Fixes + +* Upgrade matrix-sdk-crypto-wasm to 1.11.0 ([#4593](https://github.com/matrix-org/matrix-js-sdk/pull/4593)). + + Changes in [35.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.0.0) (2024-12-17) ================================================================================================== ## 🚨 BREAKING CHANGES diff --git a/package.json b/package.json index 80b5846c029..40bc38dc1e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "35.0.0", + "version": "35.1.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=20.0.0" From bcf3d56bd5502a40dc09c6348f59c94b9ca0ce97 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 19 Dec 2024 14:33:25 +0000 Subject: [PATCH 33/55] Upgrade matrix-sdk-crypto-wasm to 12.1.0 (#4596) ... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 61a11f6faf8..a056339b7f1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^12.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "^12.1.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index a7cb2097ccb..084f6f0f426 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,10 +1477,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a" - integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q== +"@matrix-org/matrix-sdk-crypto-wasm@^12.1.0": + version "12.1.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.1.0.tgz#2aef64eab2d30c0a1ace9c0fe876f53aa2949f14" + integrity sha512-NhJFu/8FOGjnW7mDssRUzaMSwXrYOcCqgAjZyAw9KQ9unNADKEi7KoIKe7GtrG2PWtm36y2bUf+hB8vhSY6Wdw== "@matrix-org/olm@3.2.15": version "3.2.15" From 3fcc56601b890a858794ca84da25fc8b752126ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2024 22:53:58 +0000 Subject: [PATCH 34/55] Use mapped types for account data content (#4590) * Use mapped types around account data events Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Harden types for reading account data too Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Correct empty object type Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update src/secret-storage.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- spec/integ/matrix-client-syncing.spec.ts | 7 +++ spec/integ/sliding-sync-sdk.spec.ts | 7 +++ spec/unit/crypto/secrets.spec.ts | 7 +++ spec/unit/matrix-client.spec.ts | 6 ++ spec/unit/rust-crypto/rust-crypto.spec.ts | 7 ++- spec/unit/rust-crypto/secret-storage.spec.ts | 12 ++++ spec/unit/secret-storage.spec.ts | 28 +++++++--- src/@types/event.ts | 34 ++++++++++++ src/client.ts | 25 ++++++--- src/crypto/CrossSigning.ts | 2 +- src/crypto/EncryptionSetup.ts | 33 +++++------ src/crypto/SecretStorage.ts | 8 +-- src/crypto/index.ts | 7 ++- src/models/invites-ignorer-types.ts | 58 ++++++++++++++++++++ src/models/invites-ignorer.ts | 49 +++-------------- src/rust-crypto/rust-crypto.ts | 4 +- src/rust-crypto/secret-storage.ts | 4 +- src/secret-storage.ts | 57 +++++++++---------- 18 files changed, 235 insertions(+), 120 deletions(-) create mode 100644 src/models/invites-ignorer-types.ts diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 2c0ab315bed..e8b9d6e52f2 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -50,6 +50,13 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread"; import { IActionsObject } from "../../src/pushprocessor"; import { KnownMembership } from "../../src/@types/membership"; +declare module "../../src/@types/event" { + interface AccountDataEvents { + a: {}; + b: {}; + } +} + describe("MatrixClient syncing", () => { const selfUserId = "@alice:localhost"; const selfAccessToken = "aseukfgwef"; diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 7b080351a6f..643f4f7e1ba 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -45,6 +45,13 @@ import { emitPromise } from "../test-utils/test-utils"; import { defer } from "../../src/utils"; import { KnownMembership } from "../../src/@types/membership"; +declare module "../../src/@types/event" { + interface AccountDataEvents { + global_test: {}; + tester: {}; + } +} + describe("SlidingSyncSdk", () => { let client: MatrixClient | undefined; let httpBackend: MockHttpBackend | undefined; diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index 88b011870f0..097ee2b1b19 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -30,6 +30,7 @@ import { ICurve25519AuthData } from "../../../src/crypto/keybackup"; import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; import { decodeBase64 } from "../../../src/base64"; import { CrossSigningKeyInfo } from "../../../src/crypto-api"; +import { SecretInfo } from "../../../src/secret-storage.ts"; async function makeTestClient( userInfo: { userId: string; deviceId: string }, @@ -68,6 +69,12 @@ function sign( }; } +declare module "../../../src/@types/event" { + interface SecretStorageAccountDataEvents { + foo: SecretInfo; + } +} + describe("Secrets", function () { if (!globalThis.Olm) { logger.warn("Not running megolm backup unit tests: libolm not present"); diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 6096090414e..8e55a0c65e5 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -94,6 +94,12 @@ function convertQueryDictToMap(queryDict?: QueryDict): Map { return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)])); } +declare module "../../src/@types/event" { + interface AccountDataEvents { + "im.vector.test": {}; + } +} + type HttpLookup = { method: string; path: string; diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index 8e177097bfe..1b56dfccbc3 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -30,6 +30,7 @@ import fetchMock from "fetch-mock-jest"; import { RustCrypto } from "../../../src/rust-crypto/rust-crypto"; import { initRustCrypto } from "../../../src/rust-crypto"; import { + AccountDataEvents, Device, DeviceVerification, encodeBase64, @@ -1924,11 +1925,13 @@ class DummyAccountDataClient super(); } - public async getAccountDataFromServer>(eventType: string): Promise { + public async getAccountDataFromServer( + eventType: K, + ): Promise { const ret = this.storage.get(eventType); if (eventType) { - return ret as T; + return ret; } else { return null; } diff --git a/spec/unit/rust-crypto/secret-storage.spec.ts b/spec/unit/rust-crypto/secret-storage.spec.ts index 0667dbd72e5..e54461a26e4 100644 --- a/spec/unit/rust-crypto/secret-storage.spec.ts +++ b/spec/unit/rust-crypto/secret-storage.spec.ts @@ -19,6 +19,18 @@ import { secretStorageContainsCrossSigningKeys, } from "../../../src/rust-crypto/secret-storage"; import { ServerSideSecretStorage } from "../../../src/secret-storage"; +import { SecretInfo } from "../../../src/secret-storage.ts"; + +declare module "../../../src/@types/event" { + interface SecretStorageAccountDataEvents { + secretA: SecretInfo; + secretB: SecretInfo; + secretC: SecretInfo; + secretD: SecretInfo; + secretE: SecretInfo; + Unknown: SecretInfo; + } +} describe("secret-storage", () => { describe("secretStorageContainsCrossSigningKeys", () => { diff --git a/spec/unit/secret-storage.spec.ts b/spec/unit/secret-storage.spec.ts index b2346d88e6c..6299141829e 100644 --- a/spec/unit/secret-storage.spec.ts +++ b/spec/unit/secret-storage.spec.ts @@ -27,6 +27,14 @@ import { trimTrailingEquals, } from "../../src/secret-storage"; import { randomString } from "../../src/randomstring"; +import { SecretInfo } from "../../src/secret-storage.ts"; +import { AccountDataEvents } from "../../src"; + +declare module "../../src/@types/event" { + interface SecretStorageAccountDataEvents { + mysecret: SecretInfo; + } +} describe("ServerSideSecretStorageImpl", function () { describe(".addKey", function () { @@ -117,9 +125,11 @@ describe("ServerSideSecretStorageImpl", function () { const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1; - async function mockGetAccountData>(eventType: string): Promise { + async function mockGetAccountData( + eventType: string, + ): Promise { if (eventType === "m.secret_storage.key.my_key") { - return storedKey as unknown as T; + return storedKey as any; } else { throw new Error(`unexpected eventType ${eventType}`); } @@ -135,11 +145,13 @@ describe("ServerSideSecretStorageImpl", function () { const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1; - async function mockGetAccountData>(eventType: string): Promise { + async function mockGetAccountData( + eventType: string, + ): Promise { if (eventType === "m.secret_storage.default_key") { - return { key: "default_key_id" } as unknown as T; + return { key: "default_key_id" } as any; } else if (eventType === "m.secret_storage.key.default_key_id") { - return storedKey as unknown as T; + return storedKey as any; } else { throw new Error(`unexpected eventType ${eventType}`); } @@ -236,9 +248,11 @@ describe("ServerSideSecretStorageImpl", function () { // stub out getAccountData to return a key with an unknown algorithm const storedKey = { algorithm: "badalg" } as SecretStorageKeyDescriptionCommon; - async function mockGetAccountData>(eventType: string): Promise { + async function mockGetAccountData( + eventType: string, + ): Promise { if (eventType === "m.secret_storage.key.keyid") { - return storedKey as unknown as T; + return storedKey as any; } else { throw new Error(`unexpected eventType ${eventType}`); } diff --git a/src/@types/event.ts b/src/@types/event.ts index 0d28b38fc32..fe7150652f5 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -58,6 +58,10 @@ import { import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts"; import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts"; import { SessionMembershipData } from "../matrixrtc/CallMembership.ts"; +import { LocalNotificationSettings } from "./local_notifications.ts"; +import { IPushRules } from "./PushRules.ts"; +import { SecretInfo, SecretStorageKeyDescription } from "../secret-storage.ts"; +import { POLICIES_ACCOUNT_EVENT_TYPE } from "../models/invites-ignorer-types.ts"; export enum EventType { // Room state events @@ -368,3 +372,33 @@ export interface StateEvents { // MSC3672 [M_BEACON_INFO.name]: MBeaconInfoEventContent; } + +/** + * Mapped type from event type to content type for all specified global account_data events. + */ +export interface AccountDataEvents extends SecretStorageAccountDataEvents { + [EventType.PushRules]: IPushRules; + [EventType.Direct]: { [userId: string]: string[] }; + [EventType.IgnoredUserList]: { [userId: string]: {} }; + "m.secret_storage.default_key": { key: string }; + "m.identity_server": { base_url: string | null }; + [key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings; + [key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription; + + // Invites-ignorer events + [POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any }; + [POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any }; +} + +/** + * Mapped type from event type to content type for all specified global events encrypted by secret storage. + * + * See https://spec.matrix.org/v1.13/client-server-api/#msecret_storagev1aes-hmac-sha2-1 + */ +export interface SecretStorageAccountDataEvents { + "m.megolm_backup.v1": SecretInfo; + "m.cross_signing.master": SecretInfo; + "m.cross_signing.self_signing": SecretInfo; + "m.cross_signing.user_signing": SecretInfo; + "org.matrix.msc3814": SecretInfo; +} diff --git a/src/client.ts b/src/client.ts index 54f53094282..eea8faa708f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -136,6 +136,7 @@ import { UpdateDelayedEventAction, } from "./@types/requests.ts"; import { + AccountDataEvents, EventType, LOCAL_NOTIFICATION_SETTINGS_PREFIX, MSC3912_RELATION_BASED_REDACTIONS_PROP, @@ -232,6 +233,7 @@ import { import { DeviceInfoMap } from "./crypto/DeviceList.ts"; import { AddSecretStorageKeyOpts, + SecretStorageKey, SecretStorageKeyDescription, ServerSideSecretStorage, ServerSideSecretStorageImpl, @@ -3070,7 +3072,7 @@ export class MatrixClient extends TypedEventEmitter | null> { + public isSecretStored(name: SecretStorageKey): Promise | null> { return this.secretStorage.isStored(name); } @@ -4236,7 +4238,10 @@ export class MatrixClient extends TypedEventEmitter { + public setAccountData( + eventType: K, + content: AccountDataEvents[K] | Record, + ): Promise<{}> { const path = utils.encodeUri("/user/$userId/account_data/$type", { $userId: this.credentials.userId!, $type: eventType, @@ -4251,7 +4256,7 @@ export class MatrixClient extends TypedEventEmitter(eventType: K): MatrixEvent | undefined { return this.store.getAccountData(eventType); } @@ -4263,7 +4268,9 @@ export class MatrixClient extends TypedEventEmitter(eventType: string): Promise { + public async getAccountDataFromServer( + eventType: K, + ): Promise { if (this.isInitialSyncComplete()) { const event = this.store.getAccountData(eventType); if (!event) { @@ -4271,7 +4278,7 @@ export class MatrixClient extends TypedEventEmitter(); + return event.getContent(); } const path = utils.encodeUri("/user/$userId/account_data/$type", { $userId: this.credentials.userId!, @@ -4287,7 +4294,7 @@ export class MatrixClient extends TypedEventEmitter { + public async deleteAccountData(eventType: keyof AccountDataEvents): Promise { const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion); // if deletion is not supported overwrite with empty content if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) { @@ -4310,7 +4317,7 @@ export class MatrixClient extends TypedEventEmitter { content.ignored_users[u] = {}; }); - return this.setAccountData("m.ignored_user_list", content); + return this.setAccountData(EventType.IgnoredUserList, content); } /** @@ -9264,7 +9271,7 @@ export class MatrixClient extends TypedEventEmitter { - const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`; + const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const; return this.setAccountData(key, notificationSettings); } diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 77abb6b2545..b3c5183f535 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -184,7 +184,7 @@ export class CrossSigningInfo { } } } - for (const type of ["self_signing", "user_signing"]) { + for (const type of ["self_signing", "user_signing"] as const) { intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {}); } return Object.keys(stored).length ? stored : null; diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index bc51dd5ef3b..84831617775 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -25,6 +25,7 @@ import { IKeyBackupInfo } from "./keybackup.ts"; import { TypedEventEmitter } from "../models/typed-event-emitter.ts"; import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage.ts"; import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api/index.ts"; +import { AccountDataEvents } from "../@types/event.ts"; interface ICrossSigningKeys { authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; @@ -111,7 +112,10 @@ export class EncryptionSetupBuilder { userSignatures[deviceId] = signature; } - public async setAccountData(type: string, content: object): Promise { + public async setAccountData( + type: K, + content: AccountDataEvents[K], + ): Promise { await this.accountDataClientAdapter.setAccountData(type, content); } @@ -160,7 +164,7 @@ export class EncryptionSetupOperation { /** */ public constructor( - private readonly accountData: Map, + private readonly accountData: Map, private readonly crossSigningKeys?: ICrossSigningKeys, private readonly keyBackupInfo?: IKeyBackupInfo, private readonly keySignatures?: KeySignatures, @@ -190,7 +194,7 @@ export class EncryptionSetupOperation { // set account data if (this.accountData) { for (const [type, content] of this.accountData) { - await baseApis.setAccountData(type, content); + await baseApis.setAccountData(type, content.getContent()); } } // upload first cross-signing signatures with the new key @@ -236,7 +240,7 @@ class AccountDataClientAdapter implements AccountDataClient { // - public readonly values = new Map(); + public readonly values = new Map(); /** * @param existingValues - existing account data @@ -248,33 +252,26 @@ class AccountDataClientAdapter /** * @returns the content of the account data */ - public getAccountDataFromServer(type: string): Promise { + public getAccountDataFromServer(type: K): Promise { return Promise.resolve(this.getAccountData(type)); } /** * @returns the content of the account data */ - public getAccountData(type: string): T | null { - const modifiedValue = this.values.get(type); - if (modifiedValue) { - return modifiedValue as unknown as T; - } - const existingValue = this.existingValues.get(type); - if (existingValue) { - return existingValue.getContent(); - } - return null; + public getAccountData(type: K): AccountDataEvents[K] | null { + const event = this.values.get(type) ?? this.existingValues.get(type); + return event?.getContent() ?? null; } - public setAccountData(type: string, content: any): Promise<{}> { + public setAccountData(type: K, content: AccountDataEvents[K]): Promise<{}> { + const event = new MatrixEvent({ type, content }); const lastEvent = this.values.get(type); - this.values.set(type, content); + this.values.set(type, event); // ensure accountData is emitted on the next tick, // as SecretStorage listens for it while calling this method // and it seems to rely on this. return Promise.resolve().then(() => { - const event = new MatrixEvent({ type, content }); this.emit(ClientEvent.AccountData, event, lastEvent); return {}; }); diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index 6bd653dd0c0..adfe63efcd2 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -25,12 +25,12 @@ import { AccountDataClient, ServerSideSecretStorage, ServerSideSecretStorageImpl, + SecretStorageKey, } from "../secret-storage.ts"; import { ISecretRequest, SecretSharing } from "./SecretSharing.ts"; /* re-exports for backwards compatibility */ export type { - AccountDataClient as IAccountDataClient, SecretStorageKeyTuple, SecretStorageKeyObject, SECRET_STORAGE_ALGORITHM_V1_AES, @@ -101,21 +101,21 @@ export class SecretStorage im /** * Store an encrypted secret on the server */ - public store(name: string, secret: string, keys?: string[] | null): Promise { + public store(name: SecretStorageKey, secret: string, keys?: string[] | null): Promise { return this.storageImpl.store(name, secret, keys); } /** * Get a secret from storage. */ - public get(name: string): Promise { + public get(name: SecretStorageKey): Promise { return this.storageImpl.get(name); } /** * Check if a secret is stored on the server. */ - public async isStored(name: string): Promise | null> { + public async isStored(name: SecretStorageKey): Promise | null> { return this.storageImpl.isStored(name); } diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 557c1f5cad0..3af8d864364 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -77,6 +77,7 @@ import { AddSecretStorageKeyOpts, calculateKeyCheck, SECRET_STORAGE_ALGORITHM_V1_AES, + SecretStorageKey, SecretStorageKeyDescription, SecretStorageKeyObject, SecretStorageKeyTuple, @@ -1194,21 +1195,21 @@ export class Crypto extends TypedEventEmitter { + public storeSecret(name: SecretStorageKey, secret: string, keys?: string[]): Promise { return this.secretStorage.store(name, secret, keys); } /** * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#get}. */ - public getSecret(name: string): Promise { + public getSecret(name: SecretStorageKey): Promise { return this.secretStorage.get(name); } /** * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}. */ - public isSecretStored(name: string): Promise | null> { + public isSecretStored(name: SecretStorageKey): Promise | null> { return this.secretStorage.isStored(name); } diff --git a/src/models/invites-ignorer-types.ts b/src/models/invites-ignorer-types.ts new file mode 100644 index 00000000000..34d58432f0b --- /dev/null +++ b/src/models/invites-ignorer-types.ts @@ -0,0 +1,58 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { UnstableValue } from "matrix-events-sdk"; + +/// The event type storing the user's individual policies. +/// +/// Exported for testing purposes. +export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies"); + +/// The key within the user's individual policies storing the user's ignored invites. +/// +/// Exported for testing purposes. +export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue( + "m.ignore.invites", + "org.matrix.msc3847.ignore.invites", +); + +/// The types of recommendations understood. +export enum PolicyRecommendation { + Ban = "m.ban", +} + +/** + * The various scopes for policies. + */ +export enum PolicyScope { + /** + * The policy deals with an individual user, e.g. reject invites + * from this user. + */ + User = "m.policy.user", + + /** + * The policy deals with a room, e.g. reject invites towards + * a specific room. + */ + Room = "m.policy.room", + + /** + * The policy deals with a server, e.g. reject invites from + * this server. + */ + Server = "m.policy.server", +} diff --git a/src/models/invites-ignorer.ts b/src/models/invites-ignorer.ts index 31d54244330..2702fd02c7b 100644 --- a/src/models/invites-ignorer.ts +++ b/src/models/invites-ignorer.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { UnstableValue } from "matrix-events-sdk"; - import { MatrixClient } from "../client.ts"; import { IContent, MatrixEvent } from "./event.ts"; import { EventTimeline } from "./event-timeline.ts"; @@ -23,47 +21,14 @@ import { Preset } from "../@types/partials.ts"; import { globToRegexp } from "../utils.ts"; import { Room } from "./room.ts"; import { EventType, StateEvents } from "../@types/event.ts"; +import { + IGNORE_INVITES_ACCOUNT_EVENT_KEY, + POLICIES_ACCOUNT_EVENT_TYPE, + PolicyRecommendation, + PolicyScope, +} from "./invites-ignorer-types.ts"; -/// The event type storing the user's individual policies. -/// -/// Exported for testing purposes. -export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies"); - -/// The key within the user's individual policies storing the user's ignored invites. -/// -/// Exported for testing purposes. -export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue( - "m.ignore.invites", - "org.matrix.msc3847.ignore.invites", -); - -/// The types of recommendations understood. -export enum PolicyRecommendation { - Ban = "m.ban", -} - -/** - * The various scopes for policies. - */ -export enum PolicyScope { - /** - * The policy deals with an individual user, e.g. reject invites - * from this user. - */ - User = "m.policy.user", - - /** - * The policy deals with a room, e.g. reject invites towards - * a specific room. - */ - Room = "m.policy.room", - - /** - * The policy deals with a server, e.g. reject invites from - * this server. - */ - Server = "m.policy.server", -} +export { IGNORE_INVITES_ACCOUNT_EVENT_KEY, POLICIES_ACCOUNT_EVENT_TYPE, PolicyRecommendation, PolicyScope }; const scopeToEventTypeMap: Record = { [PolicyScope.User]: EventType.PolicyRuleUser, diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index e586510440e..02fe6270a17 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -71,7 +71,7 @@ import { import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts"; import { Device, DeviceMap } from "../models/device.ts"; -import { SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage.ts"; +import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorageKey, ServerSideSecretStorage } from "../secret-storage.ts"; import { CrossSigningIdentity } from "./CrossSigningIdentity.ts"; import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts"; import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts"; @@ -770,7 +770,7 @@ export class RustCrypto extends TypedEventEmitter { // make sure that the cross-signing keys are stored - const secretsToCheck = [ + const secretsToCheck: SecretStorageKey[] = [ "m.cross_signing.master", "m.cross_signing.user_signing", "m.cross_signing.self_signing", diff --git a/src/rust-crypto/secret-storage.ts b/src/rust-crypto/secret-storage.ts index 951eae762df..b4b1083d0f4 100644 --- a/src/rust-crypto/secret-storage.ts +++ b/src/rust-crypto/secret-storage.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ServerSideSecretStorage } from "../secret-storage.ts"; +import { SecretStorageKey, ServerSideSecretStorage } from "../secret-storage.ts"; /** * Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the default secret storage key. @@ -44,7 +44,7 @@ export async function secretStorageContainsCrossSigningKeys(secretStorage: Serve */ export async function secretStorageCanAccessSecrets( secretStorage: ServerSideSecretStorage, - secretNames: string[], + secretNames: SecretStorageKey[], ): Promise { const defaultKeyId = await secretStorage.getDefaultKeyId(); if (!defaultKeyId) return false; diff --git a/src/secret-storage.ts b/src/secret-storage.ts index 2aa8a028aa7..2d6cd8a2793 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -28,6 +28,7 @@ import { logger } from "./logger.ts"; import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts"; import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts"; import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts"; +import { AccountDataEvents, SecretStorageAccountDataEvents } from "./@types/event.ts"; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -138,7 +139,7 @@ export interface AccountDataClient extends TypedEventEmitter>(eventType: string) => Promise; + getAccountDataFromServer: (eventType: K) => Promise; /** * Set account data event for the current user, with retries @@ -147,7 +148,7 @@ export interface AccountDataClient extends TypedEventEmitter Promise<{}>; + setAccountData: (eventType: K, content: AccountDataEvents[K]) => Promise<{}>; } /** @@ -200,7 +201,17 @@ export interface SecretStorageCallbacks { ) => Promise<[string, Uint8Array] | null>; } -interface SecretInfo { +/** + * Account Data event types which can store secret-storage-encrypted information. + */ +export type SecretStorageKey = keyof SecretStorageAccountDataEvents; + +/** + * Account Data event content type for storing secret-storage-encrypted information. + * + * See https://spec.matrix.org/v1.13/client-server-api/#msecret_storagev1aes-hmac-sha2-1 + */ +export interface SecretInfo { encrypted: { [keyId: string]: AESEncryptedSecretStoragePayload; }; @@ -293,7 +304,7 @@ export interface ServerSideSecretStorage { * with, or null if it is not present or not encrypted with a trusted * key */ - isStored(name: string): Promise | null>; + isStored(name: SecretStorageKey): Promise | null>; /** * Get the current default key ID for encrypting secrets. @@ -340,11 +351,9 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { * @returns The default key ID or null if no default key ID is set */ public async getDefaultKeyId(): Promise { - const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>( - "m.secret_storage.default_key", - ); + const defaultKey = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.default_key"); if (!defaultKey) return null; - return defaultKey.key; + return defaultKey.key ?? null; } /** @@ -409,11 +418,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { if (!keyId) { do { keyId = randomString(32); - } while ( - await this.accountDataAdapter.getAccountDataFromServer( - `m.secret_storage.key.${keyId}`, - ) - ); + } while (await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`)); } await this.accountDataAdapter.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo); @@ -441,9 +446,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { return null; } - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( - "m.secret_storage.key." + keyId, - ); + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`); return keyInfo ? [keyId, keyInfo] : null; } @@ -492,7 +495,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { * @param secret - The secret contents. * @param keys - The IDs of the keys to use to encrypt the secret, or null/undefined to use the default key. */ - public async store(name: string, secret: string, keys?: string[] | null): Promise { + public async store(name: SecretStorageKey, secret: string, keys?: string[] | null): Promise { const encrypted: Record = {}; if (!keys) { @@ -509,9 +512,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { for (const keyId of keys) { // get key information from key storage - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( - "m.secret_storage.key." + keyId, - ); + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`); if (!keyInfo) { throw new Error("Unknown key: " + keyId); } @@ -542,8 +543,8 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { * @returns the decrypted contents of the secret, or "undefined" if `name` is not found in * the user's account data. */ - public async get(name: string): Promise { - const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); + public async get(name: SecretStorageKey): Promise { + const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo) { return; } @@ -555,9 +556,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { const keys: Record = {}; for (const keyId of Object.keys(secretInfo.encrypted)) { // get key information from key storage - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( - "m.secret_storage.key." + keyId, - ); + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`); const encInfo = secretInfo.encrypted[keyId]; // only use keys we understand the encryption algorithm of if (keyInfo?.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { @@ -590,9 +589,9 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { * with, or null if it is not present or not encrypted with a trusted * key */ - public async isStored(name: string): Promise | null> { + public async isStored(name: SecretStorageKey): Promise | null> { // check if secret exists - const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); + const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo?.encrypted) return null; const ret: Record = {}; @@ -600,9 +599,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { // filter secret encryption keys with supported algorithm for (const keyId of Object.keys(secretInfo.encrypted)) { // get key information from key storage - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer( - "m.secret_storage.key." + keyId, - ); + const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`); if (!keyInfo) continue; const encInfo = secretInfo.encrypted[keyId]; From 6f7c74f9eaeafb4443ffffb32f5e3573e7c50bf6 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 3 Jan 2025 19:49:57 +0100 Subject: [PATCH 35/55] Add syntax & type check for Node.js example on CI (#4410) * Add syntax & type check for Node.js example on CI Signed-off-by: Johannes Marbach * Fix quotes --------- Signed-off-by: Johannes Marbach Co-authored-by: Florian Duros --- .github/workflows/static_analysis.yml | 35 +++++++++++++++++++++++++++ examples/node/app.js | 24 +++++++----------- examples/node/package.json | 7 ++++++ examples/node/tsconfig.json | 14 +++++++++++ 4 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 examples/node/tsconfig.json diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index c81583f05ab..7c53f80be31 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -44,6 +44,41 @@ jobs: - name: Run Linter run: "yarn run lint:js" + node_example_lint: + name: "Node.js example" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + cache: "yarn" + node-version-file: package.json + + - name: Install Deps + run: "yarn install" + + - name: Build Types + run: "yarn build:types" + + - uses: actions/setup-node@v4 + with: + cache: "npm" + node-version-file: "examples/node/package.json" + # cache-dependency-path: '**/package-lock.json' + + - name: Install Example Deps + run: "npm install" + working-directory: "examples/node" + + - name: Check Syntax + run: "node --check app.js" + working-directory: "examples/node" + + - name: Typecheck + run: "npx tsc" + working-directory: "examples/node" + workflow_lint: name: "Workflow Lint" runs-on: ubuntu-24.04 diff --git a/examples/node/app.js b/examples/node/app.js index 8f38c34cdbe..0a16e5a4d2c 100644 --- a/examples/node/app.js +++ b/examples/node/app.js @@ -94,20 +94,14 @@ rl.on("line", function (line) { ); } else if (line.indexOf("/file ") === 0) { var filename = line.split(" ")[1].trim(); - var stream = fs.createReadStream(filename); - matrixClient - .uploadContent({ - stream: stream, - name: filename, - }) - .then(function (url) { - var content = { - msgtype: MsgType.File, - body: filename, - url: JSON.parse(url).content_uri, - }; - matrixClient.sendMessage(viewingRoom.roomId, content); + let buffer = fs.readFileSync("./your_file_name"); + matrixClient.uploadContent(new Blob([buffer])).then(function (response) { + matrixClient.sendMessage(viewingRoom.roomId, { + msgtype: MsgType.File, + body: filename, + url: response.content_uri, }); + }); } else { matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () { printMessages(); @@ -167,7 +161,7 @@ matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) { if (toStartOfTimeline) { return; // don't print paginated results } - if (!viewingRoom || viewingRoom.roomId !== room.roomId) { + if (!viewingRoom || viewingRoom.roomId !== room?.roomId) { return; // not viewing a room or viewing the wrong room. } printLine(event); @@ -386,7 +380,7 @@ function print(str, formatter) { } console.log.apply(console.log, newArgs); } else { - console.log.apply(console.log, arguments); + console.log.apply(console.log, [...arguments]); } } diff --git a/examples/node/package.json b/examples/node/package.json index df42d05121b..bcc1367ff97 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -9,5 +9,12 @@ "dependencies": { "cli-color": "^1.0.0", "matrix-js-sdk": "^34.5.0" + }, + "devDependencies": { + "@types/cli-color": "^2.0.6", + "typescript": "^5.6.2" + }, + "engines": { + "node": ">=20.0.0" } } diff --git a/examples/node/tsconfig.json b/examples/node/tsconfig.json new file mode 100644 index 00000000000..78affbfd13d --- /dev/null +++ b/examples/node/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "commonjs", + "esModuleInterop": true, + "noImplicitAny": false, + "noEmit": true, + "skipLibCheck": true, + "allowJs": true, + "checkJs": true, + "strict": true + }, + "include": ["app.js"] +} From 7678923e04f917f065b721f0081750f779c1a0c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Jan 2025 13:25:29 +0000 Subject: [PATCH 36/55] Don't retry on 4xx responses (#4601) * Don't retry on 4xx responses I'm not sure why this was limited to a small set of 4xx responses. Nominally, no 4xx request should be retried (in fact the comment below says this, but then the code didn't quite match it). This was causing key backup requests to be retried even when the server responded 404 because the backup in question had been deleted, meaning the client would retry uselessly and it would take longer for the client to prompt the user for action. * Exclude 429s --- src/http-api/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http-api/utils.ts b/src/http-api/utils.ts index d23e840bcd9..ef69c7e281a 100644 --- a/src/http-api/utils.ts +++ b/src/http-api/utils.ts @@ -180,8 +180,8 @@ export function calculateRetryBackoff(err: any, attempts: number, retryConnectio return -1; } - if (err.httpStatus && (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401)) { - // client error; no amount of retrying will save you now. + if (err.httpStatus && Math.floor(err.httpStatus / 100) === 4 && err.httpStatus !== 429) { + // client error; no amount of retrying will save you now (except for rate limiting which is handled below) return -1; } From ffd3c9575e9def576739baf6b1dc329b0db55c0c Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:23:16 +0100 Subject: [PATCH 37/55] Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership (#4583) * remove all legacy call related code and adjust tests. We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests. * dont adjust tests but remove legacy tests * Remove deprecated CallMembership.getLocalExpiry() * Remove references to legacy in test case names * Clean up SessionMembershipData tsdoc * Remove CallMembership.expires * Use correct expire duration. * make expiration methods not return optional values and update docstring * add docs to `SessionMembershipData` * Use `MSC4143` (instaed of `non-legacy`) wording in comment Co-authored-by: Hugh Nimmo-Smith * Incorporate feedback from review * Fix test name --------- Co-authored-by: Hugh Nimmo-Smith Co-authored-by: Hugh Nimmo-Smith --- spec/unit/matrixrtc/CallMembership.spec.ts | 165 ++----- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 467 +++--------------- .../matrixrtc/MatrixRTCSessionManager.spec.ts | 24 +- spec/unit/matrixrtc/mocks.ts | 73 ++- src/@types/event.ts | 11 +- src/matrixrtc/CallMembership.ts | 176 +++---- src/matrixrtc/MatrixRTCSession.ts | 238 +-------- src/webrtc/groupCall.ts | 6 - 8 files changed, 241 insertions(+), 919 deletions(-) diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts index c3281b96ac3..52e6682e592 100644 --- a/spec/unit/matrixrtc/CallMembership.spec.ts +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { MatrixEvent } from "../../../src"; -import { CallMembership, CallMembershipDataLegacy, SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { CallMembership, SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; function makeMockEvent(originTs = 0): MatrixEvent { return { @@ -25,91 +25,15 @@ function makeMockEvent(originTs = 0): MatrixEvent { } describe("CallMembership", () => { - describe("CallMembershipDataLegacy", () => { - const membershipTemplate: CallMembershipDataLegacy = { - call_id: "", - scope: "m.room", - application: "m.call", - device_id: "AAAAAAA", - expires: 5000, - membershipID: "bloop", - foci_active: [{ type: "livekit" }], - }; - it("rejects membership with no expiry and no expires_ts", () => { - expect(() => { - new CallMembership( - makeMockEvent(), - Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }), - ); - }).toThrow(); - }); - - it("rejects membership with no device_id", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined })); - }).toThrow(); - }); - - it("rejects membership with no call_id", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined })); - }).toThrow(); - }); - - it("allow membership with no scope", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); - }).not.toThrow(); - }); - it("rejects with malformatted expires_ts", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" })); - }).toThrow(); - }); - it("rejects with malformatted expires", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" })); - }).toThrow(); - }); - - it("uses event timestamp if no created_ts", () => { - const membership = new CallMembership(makeMockEvent(12345), membershipTemplate); - expect(membership.createdTs()).toEqual(12345); - }); - - it("uses created_ts if present", () => { - const membership = new CallMembership( - makeMockEvent(12345), - Object.assign({}, membershipTemplate, { created_ts: 67890 }), - ); - expect(membership.createdTs()).toEqual(67890); - }); - - it("computes absolute expiry time based on expires", () => { - const membership = new CallMembership(makeMockEvent(1000), membershipTemplate); - expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); - }); - - it("computes absolute expiry time based on expires_ts", () => { - const membership = new CallMembership( - makeMockEvent(1000), - Object.assign({}, membershipTemplate, { expires_ts: 6000 }), - ); - expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); + describe("SessionMembershipData", () => { + beforeEach(() => { + jest.useFakeTimers(); }); - it("returns preferred foci", () => { - const fakeEvent = makeMockEvent(); - const mockFocus = { type: "this_is_a_mock_focus" }; - const membership = new CallMembership( - fakeEvent, - Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }), - ); - expect(membership.getPreferredFoci()).toEqual([mockFocus]); + afterEach(() => { + jest.useRealTimers(); }); - }); - describe("SessionMembershipData", () => { const membershipTemplate: SessionMembershipData = { call_id: "", scope: "m.room", @@ -150,13 +74,6 @@ describe("CallMembership", () => { expect(membership.createdTs()).toEqual(67890); }); - it("considers memberships unexpired if local age low enough", () => { - const fakeEvent = makeMockEvent(1000); - fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000); - const membership = new CallMembership(fakeEvent, membershipTemplate); - expect(membership.isExpired()).toEqual(false); - }); - it("returns preferred foci", () => { const fakeEvent = makeMockEvent(); const mockFocus = { type: "this_is_a_mock_focus" }; @@ -168,49 +85,29 @@ describe("CallMembership", () => { }); }); - describe("expiry calculation", () => { - let fakeEvent: MatrixEvent; - let membership: CallMembership; - const membershipTemplate: CallMembershipDataLegacy = { - call_id: "", - scope: "m.room", - application: "m.call", - device_id: "AAAAAAA", - expires: 5000, - membershipID: "bloop", - foci_active: [{ type: "livekit" }], - }; - - beforeEach(() => { - // server origin timestamp for this event is 1000 - fakeEvent = makeMockEvent(1000); - membership = new CallMembership(fakeEvent!, membershipTemplate); - - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it("converts expiry time into local clock", () => { - // our clock would have been at 2000 at the creation time (our clock at event receive time - age) - // (ie. the local clock is 1 second ahead of the servers' clocks) - fakeEvent.localTimestamp = 2000; - - // for simplicity's sake, we say that the event's age is zero - fakeEvent.getLocalAge = jest.fn().mockReturnValue(0); - - // for sanity's sake, make sure the server-relative expiry time is what we expect - expect(membership.getAbsoluteExpiry()).toEqual(6000); - // therefore the expiry time converted to our clock should be 1 second later - expect(membership.getLocalExpiry()).toEqual(7000); - }); - - it("calculates time until expiry", () => { - jest.setSystemTime(2000); - // should be using absolute expiry time - expect(membership.getMsUntilExpiry()).toEqual(4000); - }); - }); + // TODO: re-enable this test when expiry is implemented + // eslint-disable-next-line jest/no-commented-out-tests + // describe("expiry calculation", () => { + // let fakeEvent: MatrixEvent; + // let membership: CallMembership; + + // beforeEach(() => { + // // server origin timestamp for this event is 1000 + // fakeEvent = makeMockEvent(1000); + // membership = new CallMembership(fakeEvent!, membershipTemplate); + + // jest.useFakeTimers(); + // }); + + // afterEach(() => { + // jest.useRealTimers(); + // }); + + // eslint-disable-next-line jest/no-commented-out-tests + // it("calculates time until expiry", () => { + // jest.setSystemTime(2000); + // // should be using absolute expiry time + // expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000); + // }); + // }); }); diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 248be4c19ec..d3755744108 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,27 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeBase64, EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src"; +import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src"; import { KnownMembership } from "../../../src/@types/membership"; -import { - CallMembershipData, - CallMembershipDataLegacy, - SessionMembershipData, -} from "../../../src/matrixrtc/CallMembership"; +import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; import { randomString } from "../../../src/randomstring"; -import { makeMockRoom, makeMockRoomState, mockRTCEvent } from "./mocks"; - -const membershipTemplate: CallMembershipData = { - call_id: "", - scope: "m.room", - application: "m.call", - device_id: "AAAAAAA", - expires: 60 * 60 * 1000, - membershipID: "bloop", - foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }], -}; +import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks"; const mockFocus = { type: "mock" }; @@ -59,7 +45,7 @@ describe("MatrixRTCSession", () => { describe("roomSessionForRoom", () => { it("creates a room-scoped session from room state", () => { - const mockRoom = makeMockRoom([membershipTemplate]); + const mockRoom = makeMockRoom(membershipTemplate); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(1); @@ -67,43 +53,46 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships[0].scope).toEqual("m.room"); expect(sess?.memberships[0].application).toEqual("m.call"); expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); - expect(sess?.memberships[0].membershipID).toEqual("bloop"); expect(sess?.memberships[0].isExpired()).toEqual(false); expect(sess?.callId).toEqual(""); }); - it("ignores expired memberships events", () => { - jest.useFakeTimers(); - const expiredMembership = Object.assign({}, membershipTemplate); - expiredMembership.expires = 1000; - expiredMembership.device_id = "EXPIRED"; - const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]); - - jest.advanceTimersByTime(2000); - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess?.memberships.length).toEqual(1); - expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); - jest.useRealTimers(); - }); + // TODO: re-enable this test when expiry is implemented + // eslint-disable-next-line jest/no-commented-out-tests + // it("ignores expired memberships events", () => { + // jest.useFakeTimers(); + // const expiredMembership = Object.assign({}, membershipTemplate); + // expiredMembership.expires = 1000; + // expiredMembership.device_id = "EXPIRED"; + // const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]); + + // jest.advanceTimersByTime(2000); + // sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + // expect(sess?.memberships.length).toEqual(1); + // expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); + // jest.useRealTimers(); + // }); it("ignores memberships events of members not in the room", () => { - const mockRoom = makeMockRoom([membershipTemplate]); + const mockRoom = makeMockRoom(membershipTemplate); mockRoom.hasMembershipState = (state) => state === KnownMembership.Join; sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(0); }); - it("honours created_ts", () => { - jest.useFakeTimers(); - jest.setSystemTime(500); - const expiredMembership = Object.assign({}, membershipTemplate); - expiredMembership.created_ts = 500; - expiredMembership.expires = 1000; - const mockRoom = makeMockRoom([expiredMembership]); - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); - jest.useRealTimers(); - }); + // TODO: re-enable this test when expiry is implemented + // eslint-disable-next-line jest/no-commented-out-tests + // it("honours created_ts", () => { + // jest.useFakeTimers(); + // jest.setSystemTime(500); + // const expiredMembership = Object.assign({}, membershipTemplate); + // expiredMembership.created_ts = 500; + // expiredMembership.expires = 1000; + // const mockRoom = makeMockRoom([expiredMembership]); + // sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + // expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); + // jest.useRealTimers(); + // }); it("returns empty session if no membership events are present", () => { const mockRoom = makeMockRoom([]); @@ -181,14 +170,6 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores memberships with no expires_ts", () => { - const expiredMembership = Object.assign({}, membershipTemplate); - (expiredMembership.expires as number | undefined) = undefined; - const mockRoom = makeMockRoom([expiredMembership]); - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess.memberships).toHaveLength(0); - }); - it("ignores memberships with no device_id", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.device_id as string | undefined) = undefined; @@ -224,23 +205,7 @@ describe("MatrixRTCSession", () => { describe("updateCallMembershipEvent", () => { const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" }; - const joinSessionConfig = { useLegacyMemberEvents: false }; - - const legacyMembershipData: CallMembershipDataLegacy = { - call_id: "", - scope: "m.room", - application: "m.call", - device_id: "AAAAAAA_legacy", - expires: 60 * 60 * 1000, - membershipID: "bloop", - foci_active: [mockFocus], - }; - - const expiredLegacyMembershipData: CallMembershipDataLegacy = { - ...legacyMembershipData, - device_id: "AAAAAAA_legacy_expired", - expires: 0, - }; + const joinSessionConfig = {}; const sessionMembershipData: SessionMembershipData = { call_id: "", @@ -273,39 +238,22 @@ describe("MatrixRTCSession", () => { client._unstable_sendDelayedStateEvent = sendDelayedStateMock; }); - async function testSession( - membershipData: CallMembershipData[] | SessionMembershipData, - shouldUseLegacy: boolean, - ): Promise { + async function testSession(membershipData: SessionMembershipData): Promise { sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData)); - const makeNewLegacyMembershipsMock = jest.spyOn(sess as any, "makeNewLegacyMemberships"); const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership"); sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig); await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]); - expect(makeNewLegacyMembershipsMock).toHaveBeenCalledTimes(shouldUseLegacy ? 1 : 0); - expect(makeNewMembershipMock).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1); + expect(makeNewMembershipMock).toHaveBeenCalledTimes(1); await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]); - expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1); + expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1); } - it("uses legacy events if there are any active legacy calls", async () => { - await testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true); - }); - - it('uses legacy events if a non-legacy call is in a "memberships" array', async () => { - await testSession([sessionMembershipData], true); - }); - - it("uses non-legacy events if all legacy calls are expired", async () => { - await testSession([expiredLegacyMembershipData], false); - }); - - it("uses non-legacy events if there are only non-legacy calls", async () => { - await testSession(sessionMembershipData, false); + it("sends events", async () => { + await testSession(sessionMembershipData); }); }); @@ -325,70 +273,6 @@ describe("MatrixRTCSession", () => { }); }); - describe("getsActiveFocus", () => { - const activeFociConfig = { type: "livekit", livekit_service_url: "https://active.url" }; - it("gets the correct active focus with oldest_membership", () => { - jest.useFakeTimers(); - jest.setSystemTime(3000); - const mockRoom = makeMockRoom([ - Object.assign({}, membershipTemplate, { - device_id: "foo", - created_ts: 500, - foci_active: [activeFociConfig], - }), - Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), - Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }), - ]); - - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - - sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], { - type: "livekit", - focus_selection: "oldest_membership", - }); - expect(sess.getActiveFocus()).toBe(activeFociConfig); - jest.useRealTimers(); - }); - it("does not provide focus if the selction method is unknown", () => { - const mockRoom = makeMockRoom([ - Object.assign({}, membershipTemplate, { - device_id: "foo", - created_ts: 500, - foci_active: [activeFociConfig], - }), - Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), - Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }), - ]); - - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - - sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], { - type: "livekit", - focus_selection: "unknown", - }); - expect(sess.getActiveFocus()).toBe(undefined); - }); - it("gets the correct active focus legacy", () => { - jest.useFakeTimers(); - jest.setSystemTime(3000); - const mockRoom = makeMockRoom([ - Object.assign({}, membershipTemplate, { - device_id: "foo", - created_ts: 500, - foci_active: [activeFociConfig], - }), - Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), - Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }), - ]); - - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - - sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }]); - expect(sess.getActiveFocus()).toBe(activeFociConfig); - jest.useRealTimers(); - }); - }); - describe("joining", () => { let mockRoom: Room; let sendStateEventMock: jest.Mock; @@ -439,67 +323,7 @@ describe("MatrixRTCSession", () => { expect(sess!.isJoined()).toEqual(true); }); - it("sends a membership event when joining a call", async () => { - const realSetTimeout = setTimeout; - jest.useFakeTimers(); - sess!.joinRoomSession([mockFocus], mockFocus); - await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]); - expect(client.sendStateEvent).toHaveBeenCalledWith( - mockRoom!.roomId, - EventType.GroupCallMemberPrefix, - { - memberships: [ - { - application: "m.call", - scope: "m.room", - call_id: "", - device_id: "AAAAAAA", - expires: 3600000, - expires_ts: Date.now() + 3600000, - foci_active: [mockFocus], - - membershipID: expect.stringMatching(".*"), - }, - ], - }, - "@alice:example.org", - ); - await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]); - expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0); - jest.useRealTimers(); - }); - - it("uses membershipExpiryTimeout from join config", async () => { - const realSetTimeout = setTimeout; - jest.useFakeTimers(); - sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 }); - await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]); - expect(client.sendStateEvent).toHaveBeenCalledWith( - mockRoom!.roomId, - EventType.GroupCallMemberPrefix, - { - memberships: [ - { - application: "m.call", - scope: "m.room", - call_id: "", - device_id: "AAAAAAA", - expires: 60000, - expires_ts: Date.now() + 60000, - foci_active: [mockFocus], - - membershipID: expect.stringMatching(".*"), - }, - ], - }, - "@alice:example.org", - ); - await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]); - expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0); - jest.useRealTimers(); - }); - - describe("non-legacy calls", () => { + describe("calls", () => { const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" }; const activeFocus = { type: "livekit", focus_selection: "oldest_membership" }; @@ -557,7 +381,6 @@ describe("MatrixRTCSession", () => { }); sess!.joinRoomSession([activeFocusConfig], activeFocus, { - useLegacyMemberEvents: false, membershipServerSideExpiryTimeout: 9000, }); @@ -579,6 +402,7 @@ describe("MatrixRTCSession", () => { application: "m.call", scope: "m.room", call_id: "", + expires: 14400000, device_id: "AAAAAAA", foci_preferred: [activeFocusConfig], focus_active: activeFocus, @@ -598,7 +422,7 @@ describe("MatrixRTCSession", () => { jest.useRealTimers(); } - it("sends a membership event with session payload when joining a non-legacy call", async () => { + it("sends a membership event with session payload when joining a call", async () => { await testJoin(false); }); @@ -607,91 +431,19 @@ describe("MatrixRTCSession", () => { }); }); - it("does nothing if join called when already joined", () => { + it("does nothing if join called when already joined", async () => { sess!.joinRoomSession([mockFocus], mockFocus); - + await sentStateEvent; expect(client.sendStateEvent).toHaveBeenCalledTimes(1); sess!.joinRoomSession([mockFocus], mockFocus); expect(client.sendStateEvent).toHaveBeenCalledTimes(1); }); - - it("renews membership event before expiry time", async () => { - jest.useFakeTimers(); - let resolveFn: ((_roomId: string, _type: string, val: Record) => void) | undefined; - - const eventSentPromise = new Promise>((r) => { - resolveFn = (_roomId: string, _type: string, val: Record) => { - r(val); - }; - }); - try { - const sendStateEventMock = jest.fn().mockImplementation(resolveFn); - client.sendStateEvent = sendStateEventMock; - - sess!.joinRoomSession([mockFocus], mockFocus); - - const eventContent = await eventSentPromise; - - jest.setSystemTime(1000); - const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId); - const getState = mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!; - getState.getStateEvents = jest.fn().mockReturnValue(event); - getState.events = new Map([ - [ - event.getType(), - { - size: () => true, - has: (_stateKey: string) => true, - get: (_stateKey: string) => event, - values: () => [event], - } as unknown as Map, - ], - ]); - - const eventReSentPromise = new Promise>((r) => { - resolveFn = (_roomId: string, _type: string, val: Record) => { - r(val); - }; - }); - - sendStateEventMock.mockReset().mockImplementation(resolveFn); - - // definitely should have renewed by 1 second before the expiry! - const timeElapsed = 60 * 60 * 1000 - 1000; - jest.setSystemTime(Date.now() + timeElapsed); - jest.advanceTimersByTime(timeElapsed); - await eventReSentPromise; - - expect(sendStateEventMock).toHaveBeenCalledWith( - mockRoom.roomId, - EventType.GroupCallMemberPrefix, - { - memberships: [ - { - application: "m.call", - scope: "m.room", - call_id: "", - device_id: "AAAAAAA", - expires: 3600000 * 2, - expires_ts: 1000 + 3600000 * 2, - foci_active: [mockFocus], - created_ts: 1000, - membershipID: expect.stringMatching(".*"), - }, - ], - }, - "@alice:example.org", - ); - } finally { - jest.useRealTimers(); - } - }); }); describe("onMembershipsChanged", () => { it("does not emit if no membership changes", () => { - const mockRoom = makeMockRoom([membershipTemplate]); + const mockRoom = makeMockRoom(membershipTemplate); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); const onMembershipsChanged = jest.fn(); @@ -702,7 +454,7 @@ describe("MatrixRTCSession", () => { }); it("emits on membership changes", () => { - const mockRoom = makeMockRoom([membershipTemplate]); + const mockRoom = makeMockRoom(membershipTemplate); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); const onMembershipsChanged = jest.fn(); @@ -714,26 +466,28 @@ describe("MatrixRTCSession", () => { expect(onMembershipsChanged).toHaveBeenCalled(); }); - it("emits an event at the time a membership event expires", () => { - jest.useFakeTimers(); - try { - const membership = Object.assign({}, membershipTemplate); - const mockRoom = makeMockRoom([membership]); + // TODO: re-enable this test when expiry is implemented + // eslint-disable-next-line jest/no-commented-out-tests + // it("emits an event at the time a membership event expires", () => { + // jest.useFakeTimers(); + // try { + // const membership = Object.assign({}, membershipTemplate); + // const mockRoom = makeMockRoom([membership]); - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - const membershipObject = sess.memberships[0]; + // sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + // const membershipObject = sess.memberships[0]; - const onMembershipsChanged = jest.fn(); - sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); + // const onMembershipsChanged = jest.fn(); + // sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); - jest.advanceTimersByTime(61 * 1000 * 1000); + // jest.advanceTimersByTime(61 * 1000 * 1000); - expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []); - expect(sess?.memberships.length).toEqual(0); - } finally { - jest.useRealTimers(); - } - }); + // expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []); + // expect(sess?.memberships.length).toEqual(0); + // } finally { + // jest.useRealTimers(); + // } + // }); }); describe("key management", () => { @@ -805,9 +559,13 @@ describe("MatrixRTCSession", () => { } }); - it("does not send key if join called when already joined", () => { + it("does not send key if join called when already joined", async () => { + const sentStateEvent = new Promise((resolve) => { + sendStateEventMock = jest.fn(resolve); + }); + client.sendStateEvent = sendStateEventMock; sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true }); - + await sentStateEvent; expect(client.sendStateEvent).toHaveBeenCalledTimes(1); expect(client.sendEvent).toHaveBeenCalledTimes(1); expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1); @@ -1016,89 +774,6 @@ describe("MatrixRTCSession", () => { } }); - it("re-sends key if a member changes membership ID", async () => { - jest.useFakeTimers(); - try { - const keysSentPromise1 = new Promise((resolve) => { - sendEventMock.mockImplementation(resolve); - }); - - const member1 = membershipTemplate; - const member2 = { - ...membershipTemplate, - device_id: "BBBBBBB", - }; - - const mockRoom = makeMockRoom([member1, member2]); - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId)); - - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true }); - - await keysSentPromise1; - - // make sure an encryption key was sent - expect(sendEventMock).toHaveBeenCalledWith( - expect.stringMatching(".*"), - "io.element.call.encryption_keys", - { - call_id: "", - device_id: "AAAAAAA", - keys: [ - { - index: 0, - key: expect.stringMatching(".*"), - }, - ], - sent_ts: Date.now(), - }, - ); - expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1); - - sendEventMock.mockClear(); - - // this should be a no-op: - sess.onMembershipUpdate(); - expect(sendEventMock).toHaveBeenCalledTimes(0); - - // advance time to avoid key throttling - jest.advanceTimersByTime(10000); - - // update membership ID - member2.membershipID = "newID"; - - const keysSentPromise2 = new Promise((resolve) => { - sendEventMock.mockImplementation(resolve); - }); - - // this should re-send the key - sess.onMembershipUpdate(); - - await keysSentPromise2; - - expect(sendEventMock).toHaveBeenCalledWith( - expect.stringMatching(".*"), - "io.element.call.encryption_keys", - { - call_id: "", - device_id: "AAAAAAA", - keys: [ - { - index: 0, - key: expect.stringMatching(".*"), - }, - ], - sent_ts: Date.now(), - }, - ); - expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2); - } finally { - jest.useRealTimers(); - } - }); - it("re-sends key if a member changes created_ts", async () => { jest.useFakeTimers(); jest.setSystemTime(1000); @@ -1240,7 +915,7 @@ describe("MatrixRTCSession", () => { it("wraps key index around to 0 when it reaches the maximum", async () => { // this should give us keys with index [0...255, 0, 1] const membersToTest = 258; - const members: CallMembershipData[] = []; + const members: SessionMembershipData[] = []; for (let i = 0; i < membersToTest; i++) { members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` })); } diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index bbc9c9f7e6f..5b87098c0f8 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Mock } from "jest-mock"; + import { ClientEvent, EventTimeline, @@ -24,19 +26,8 @@ import { RoomEvent, } from "../../../src"; import { RoomStateEvent } from "../../../src/models/room-state"; -import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager"; -import { makeMockRoom } from "./mocks"; - -const membershipTemplate: CallMembershipData = { - call_id: "", - scope: "m.room", - application: "m.call", - device_id: "AAAAAAA", - expires: 60 * 60 * 1000, - membershipID: "bloop", - foci_active: [{ type: "test" }], -}; +import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks"; describe("MatrixRTCSessionManager", () => { let client: MatrixClient; @@ -69,16 +60,15 @@ describe("MatrixRTCSessionManager", () => { it("Fires event when session ends", () => { const onEnded = jest.fn(); client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded); - - const memberships = [membershipTemplate]; - - const room1 = makeMockRoom(memberships); + const room1 = makeMockRoom(membershipTemplate); jest.spyOn(client, "getRooms").mockReturnValue([room1]); jest.spyOn(client, "getRoom").mockReturnValue(room1); client.emit(ClientEvent.Room, room1); - memberships.splice(0, 1); + (room1.getLiveTimeline as Mock).mockReturnValue({ + getState: jest.fn().mockReturnValue(makeMockRoomState([{}], room1.roomId)), + }); const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!; const membEvent = roomState.getStateEvents("")[0]; diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index 57863dc2c38..b5aa7096017 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -15,16 +15,36 @@ limitations under the License. */ import { EventType, MatrixEvent, Room } from "../../../src"; -import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; import { randomString } from "../../../src/randomstring"; -type MembershipData = CallMembershipData[] | SessionMembershipData; +type MembershipData = SessionMembershipData[] | SessionMembershipData | {}; + +export const membershipTemplate: SessionMembershipData = { + application: "m.call", + call_id: "", + device_id: "AAAAAAA", + scope: "m.room", + focus_active: { type: "livekit", livekit_service_url: "https://lk.url" }, + foci_preferred: [ + { + livekit_alias: "!alias:something.org", + livekit_service_url: "https://livekit-jwt.something.io", + type: "livekit", + }, + { + livekit_alias: "!alias:something.org", + livekit_service_url: "https://livekit-jwt.something.dev", + type: "livekit", + }, + ], +}; export function makeMockRoom(membershipData: MembershipData): Room { const roomId = randomString(8); // Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()` const roomState = makeMockRoomState(membershipData, roomId); - return { + const room = { roomId: roomId, hasMembershipState: jest.fn().mockReturnValue(true), getLiveTimeline: jest.fn().mockReturnValue({ @@ -32,41 +52,46 @@ export function makeMockRoom(membershipData: MembershipData): Room { }), getVersion: jest.fn().mockReturnValue("default"), } as unknown as Room; + return room; } export function makeMockRoomState(membershipData: MembershipData, roomId: string) { - const event = mockRTCEvent(membershipData, roomId); + const events = Array.isArray(membershipData) + ? membershipData.map((m) => mockRTCEvent(m, roomId)) + : [mockRTCEvent(membershipData, roomId)]; + const keysAndEvents = events.map((e) => { + const data = e.getContent() as SessionMembershipData; + return [`_${e.sender?.userId}_${data.device_id}`]; + }); + return { on: jest.fn(), off: jest.fn(), getStateEvents: (_: string, stateKey: string) => { - if (stateKey !== undefined) return event; - return [event]; + if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1]; + return events; }, - events: new Map([ - [ - event.getType(), - { - size: () => true, - has: (_stateKey: string) => true, - get: (_stateKey: string) => event, - values: () => [event], - }, - ], - ]), + events: + events.length === 0 + ? new Map() + : new Map([ + [ + EventType.GroupCallMemberPrefix, + { + size: () => true, + has: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey), + get: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey)?.[1], + values: () => events, + }, + ], + ]), }; } export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent { return { getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), - getContent: jest.fn().mockReturnValue( - !Array.isArray(membershipData) - ? membershipData - : { - memberships: membershipData, - }, - ), + getContent: jest.fn().mockReturnValue(membershipData), getSender: jest.fn().mockReturnValue("@mock:user.example"), getTs: jest.fn().mockReturnValue(Date.now()), getRoomId: jest.fn().mockReturnValue(roomId), diff --git a/src/@types/event.ts b/src/@types/event.ts index fe7150652f5..8815c6f900d 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -35,11 +35,7 @@ import { SpaceChildEventContent, SpaceParentEventContent, } from "./state_events.ts"; -import { - ExperimentalGroupCallRoomMemberState, - IGroupCallRoomMemberState, - IGroupCallRoomState, -} from "../webrtc/groupCall.ts"; +import { IGroupCallRoomMemberState, IGroupCallRoomState } from "../webrtc/groupCall.ts"; import { MSC3089EventContent } from "../models/MSC3089Branch.ts"; import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon.ts"; import { XOR } from "./common.ts"; @@ -361,10 +357,7 @@ export interface StateEvents { // MSC3401 [EventType.GroupCallPrefix]: IGroupCallRoomState; - [EventType.GroupCallMemberPrefix]: XOR< - XOR, - XOR - >; + [EventType.GroupCallMemberPrefix]: XOR>; // MSC3089 [UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent; diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index 6c7efc029d6..ec6a2f4d76f 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -14,35 +14,71 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EitherAnd } from "matrix-events-sdk/lib/types"; - import { MatrixEvent } from "../matrix.ts"; import { deepCompare } from "../utils.ts"; import { Focus } from "./focus.ts"; import { isLivekitFocusActive } from "./LivekitFocus.ts"; type CallScope = "m.room" | "m.user"; -// Represents an entry in the memberships section of an m.call.member event as it is on the wire - -// There are two different data interfaces. One for the Legacy types and one compliant with MSC4143 - -// MSC4143 (MatrixRTC) session membership data +/** + * MSC4143 (MatrixRTC) session membership data. + * Represents an entry in the memberships section of an m.call.member event as it is on the wire. + **/ export type SessionMembershipData = { + /** + * The RTC application defines the type of the RTC session. + */ application: string; + + /** + * The id of this session. + * A session can never span over multiple rooms so this id is to distinguish between + * multiple session in one room. A room wide session that is not associated with a user, + * and therefore immune to creation race conflicts, uses the `call_id: ""`. + */ call_id: string; + + /** + * The Matrix device ID of this session. A single user can have multiple sessions on different devices. + */ device_id: string; + /** + * The focus selection system this user/membership is using. + */ focus_active: Focus; + + /** + * A list of possible foci this uses knows about. One of them might be used based on the focus_active + * selection system. + */ foci_preferred: Focus[]; + + /** + * Optional field that contains the creation of the session. If it is undefined the creation + * is the `origin_server_ts` of the event itself. For updates to the event this property tracks + * the `origin_server_ts` of the initial join event. + * - If it is undefined it can be interpreted as a "Join". + * - If it is defined it can be interpreted as an "Update" + */ created_ts?: number; // Application specific data + + /** + * If the `application` = `"m.call"` this defines if it is a room or user owned call. + * There can always be one room scroped call but multiple user owned calls (breakout sessions) + */ scope?: CallScope; -}; -export const isSessionMembershipData = (data: CallMembershipData): data is SessionMembershipData => - "focus_active" in data; + /** + * Optionally we allow to define a delta to the `created_ts` that defines when the event is expired/invalid. + * This should be set to multiple hours. The only reason it exist is to deal with failed delayed events. + * (for example caused by a homeserver crashes) + **/ + expires?: number; +}; const checkSessionsMembershipData = (data: any, errors: string[]): data is SessionMembershipData => { const prefix = "Malformed session membership event: "; @@ -59,65 +95,20 @@ const checkSessionsMembershipData = (data: any, errors: string[]): data is Sessi return errors.length === 0; }; -// Legacy session membership data - -export type CallMembershipDataLegacy = { - application: string; - call_id: string; - scope: CallScope; - device_id: string; - membershipID: string; - created_ts?: number; - foci_active?: Focus[]; -} & EitherAnd<{ expires: number }, { expires_ts: number }>; - -export const isLegacyCallMembershipData = (data: CallMembershipData): data is CallMembershipDataLegacy => - "membershipID" in data; - -const checkCallMembershipDataLegacy = (data: any, errors: string[]): data is CallMembershipDataLegacy => { - const prefix = "Malformed legacy rtc membership event: "; - if (!("expires" in data || "expires_ts" in data)) { - errors.push(prefix + "expires_ts or expires must be present"); - } - if ("expires" in data) { - if (typeof data.expires !== "number") { - errors.push(prefix + "expires must be numeric"); - } - } - if ("expires_ts" in data) { - if (typeof data.expires_ts !== "number") { - errors.push(prefix + "expires_ts must be numeric"); - } - } - - if (typeof data.device_id !== "string") errors.push(prefix + "device_id must be string"); - if (typeof data.call_id !== "string") errors.push(prefix + "call_id must be string"); - if (typeof data.application !== "string") errors.push(prefix + "application must be a string"); - if (typeof data.membershipID !== "string") errors.push(prefix + "membershipID must be a string"); - // optional elements - if (data.created_ts && typeof data.created_ts !== "number") errors.push(prefix + "created_ts must be number"); - // application specific data (we first need to check if they exist) - if (data.scope && typeof data.scope !== "string") errors.push(prefix + "scope must be string"); - return errors.length === 0; -}; - -export type CallMembershipData = CallMembershipDataLegacy | SessionMembershipData; - export class CallMembership { public static equal(a: CallMembership, b: CallMembership): boolean { return deepCompare(a.membershipData, b.membershipData); } - private membershipData: CallMembershipData; + private membershipData: SessionMembershipData; public constructor( private parentEvent: MatrixEvent, data: any, ) { const sessionErrors: string[] = []; - const legacyErrors: string[] = []; - if (!checkSessionsMembershipData(data, sessionErrors) && !checkCallMembershipDataLegacy(data, legacyErrors)) { + if (!checkSessionsMembershipData(data, sessionErrors)) { throw Error( - `unknown CallMembership data. Does not match legacy call.member (${legacyErrors.join(" & ")}) events nor MSC4143 (${sessionErrors.join(" & ")})`, + `unknown CallMembership data. Does not match MSC4143 call.member (${sessionErrors.join(" & ")}) events this could be a legacy membership event: (${data})`, ); } else { this.membershipData = data; @@ -149,11 +140,10 @@ export class CallMembership { } public get membershipID(): string { - if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.membershipID; // the createdTs behaves equivalent to the membershipID. // we only need the field for the legacy member envents where we needed to update them // synapse ignores sending state events if they have the same content. - else return this.createdTs().toString(); + return this.createdTs().toString(); } public createdTs(): number { @@ -161,57 +151,24 @@ export class CallMembership { } /** - * Gets the absolute expiry time of the membership if applicable to this membership type. + * Gets the absolute expiry timestamp of the membership. * @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable */ public getAbsoluteExpiry(): number | undefined { - // if the membership is not a legacy membership, we assume it is MSC4143 - if (!isLegacyCallMembershipData(this.membershipData)) return undefined; + // TODO: implement this in a future PR. Something like: + // TODO: calculate this from the MatrixRTCSession join configuration directly + // return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION); - if ("expires" in this.membershipData) { - // we know createdTs exists since we already do the isLegacyCallMembershipData check - return this.createdTs() + this.membershipData.expires; - } else { - // We know it exists because we checked for this in the constructor. - return this.membershipData.expires_ts; - } - } - - /** - * Gets the expiry time of the event, converted into the device's local time. - * @deprecated This function has been observed returning bad data and is no longer used by MatrixRTC. - * @returns The local expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable - */ - public getLocalExpiry(): number | undefined { - // if the membership is not a legacy membership, we assume it is MSC4143 - if (!isLegacyCallMembershipData(this.membershipData)) return undefined; - - if ("expires" in this.membershipData) { - // we know createdTs exists since we already do the isLegacyCallMembershipData check - const relativeCreationTime = this.parentEvent.getTs() - this.createdTs(); - - const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime; - - return localCreationTs + this.membershipData.expires; - } else { - // With expires_ts we cannot convert to local time. - // TODO: Check the server timestamp and compute a diff to local time. - return this.membershipData.expires_ts; - } + return undefined; } /** * @returns The number of milliseconds until the membership expires or undefined if applicable */ public getMsUntilExpiry(): number | undefined { - if (isLegacyCallMembershipData(this.membershipData)) { - // Assume that local clock is sufficiently in sync with other clocks in the distributed system. - // We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate. - // The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2 - return this.getAbsoluteExpiry()! - Date.now(); - } + // TODO: implement this in a future PR. Something like: + // return this.getAbsoluteExpiry() - Date.now(); - // Assumed to be MSC4143 return undefined; } @@ -219,29 +176,20 @@ export class CallMembership { * @returns true if the membership has expired, otherwise false */ public isExpired(): boolean { - if (isLegacyCallMembershipData(this.membershipData)) return this.getMsUntilExpiry()! <= 0; + // TODO: implement this in a future PR. Something like: + // return this.getMsUntilExpiry() <= 0; - // MSC4143 events expire by being updated. So if the event exists, its not expired. return false; } public getPreferredFoci(): Focus[] { - // To support both, the new and the old MatrixRTC memberships have two cases based - // on the availablitiy of `foci_preferred` - if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.foci_active ?? []; - - // MSC4143 style membership return this.membershipData.foci_preferred; } public getFocusSelection(): string | undefined { - if (isLegacyCallMembershipData(this.membershipData)) { - return "oldest_membership"; - } else { - const focusActive = this.membershipData.focus_active; - if (isLivekitFocusActive(focusActive)) { - return focusActive.focus_selection; - } + const focusActive = this.membershipData.focus_active; + if (isLivekitFocusActive(focusActive)) { + return focusActive.focus_selection; } } } diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 855596b7976..8b65771ae15 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -21,25 +21,20 @@ import { Room } from "../models/room.ts"; import { MatrixClient } from "../client.ts"; import { EventType } from "../@types/event.ts"; import { UpdateDelayedEventAction } from "../@types/requests.ts"; -import { - CallMembership, - CallMembershipData, - CallMembershipDataLegacy, - SessionMembershipData, - isLegacyCallMembershipData, -} from "./CallMembership.ts"; +import { CallMembership, SessionMembershipData } from "./CallMembership.ts"; import { RoomStateEvent } from "../models/room-state.ts"; import { Focus } from "./focus.ts"; -import { randomString, secureRandomBase64Url } from "../randomstring.ts"; +import { secureRandomBase64Url } from "../randomstring.ts"; import { EncryptionKeysEventContent } from "./types.ts"; import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; import { HTTPError, MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts"; import { MatrixEvent } from "../models/event.ts"; import { isLivekitFocusActive } from "./LivekitFocus.ts"; -import { ExperimentalGroupCallRoomMemberState } from "../webrtc/groupCall.ts"; import { sleep } from "../utils.ts"; +const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4; // 4 hours + const logger = rootLogger.getChild("MatrixRTCSession"); const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; @@ -82,14 +77,6 @@ export interface JoinSessionConfig { */ manageMediaKeys?: boolean; - /** Lets you configure how the events for the session are formatted. - * - legacy: use one event with a membership array. - * - MSC4143: use one event per membership (with only one membership per event) - * More details can be found in MSC4143 and by checking the types: - * `CallMembershipDataLegacy` and `SessionMembershipData` - */ - useLegacyMemberEvents?: boolean; - /** * The timeout (in milliseconds) after we joined the call, that our membership should expire * unless we have explicitly updated it. @@ -161,11 +148,7 @@ export class MatrixRTCSession extends TypedEventEmitter; private expiryTimeout?: ReturnType; private keysEventUpdateTimeout?: ReturnType; @@ -229,7 +204,6 @@ export class MatrixRTCSession extends TypedEventEmitter array of (key, timestamp) private encryptionKeys = new Map>(); private lastEncryptionKeyUpdateRequest?: number; @@ -292,19 +266,14 @@ export class MatrixRTCSession extends TypedEventEmitter 1 && "focus_active" in content) { // We have a MSC4143 event membership event membershipContents.push(content); } else if (eventKeysCount === 1 && "memberships" in content) { - // we have a legacy (one event for all devices) event - if (!Array.isArray(content["memberships"])) { - logger.warn(`Malformed member event from ${memberEvent.getSender()}: memberships is not an array`); - continue; - } - membershipContents = content["memberships"]; + logger.warn(`Legacy event found. Those are ignored, they do not contribute to the MatrixRTC session`); } if (membershipContents.length === 0) continue; @@ -416,8 +385,6 @@ export class MatrixRTCSession extends TypedEventEmitter !this.isMyMembership(m)) - .map((m) => `${getParticipantIdFromMembership(m)}:${m.membershipID}:${m.createdTs()}`), + .map((m) => `${getParticipantIdFromMembership(m)}:${m.createdTs()}`), ); } - /** - * Constructs our own membership - * @param prevMembership - The previous value of our call membership, if any - */ - private makeMyMembershipLegacy(deviceId: string, prevMembership?: CallMembership): CallMembershipDataLegacy { - if (this.relativeExpiry === undefined) { - throw new Error("Tried to create our own membership event when we're not joined!"); - } - if (this.membershipId === undefined) { - throw new Error("Tried to create our own membership event when we have no membership ID!"); - } - const createdTs = prevMembership?.createdTs(); - return { - call_id: "", - scope: "m.room", - application: "m.call", - device_id: deviceId, - expires: this.relativeExpiry, - // TODO: Date.now() should be the origin_server_ts (now). - expires_ts: this.relativeExpiry + (createdTs ?? Date.now()), - // we use the fociPreferred since this is the list of foci. - // it is named wrong in the Legacy events. - foci_active: this.ownFociPreferred, - membershipID: this.membershipId, - ...(createdTs ? { created_ts: createdTs } : {}), - }; - } /** * Constructs our own membership */ @@ -968,36 +907,12 @@ export class MatrixRTCSession extends TypedEventEmitter { - let membershipObj; - try { - membershipObj = new CallMembership(myCallMemberEvent!, m); - } catch { - return false; - } - - return !membershipObj.isExpired(); - }; - - const transformMemberships = (m: CallMembershipData): CallMembershipData => { - if (m.created_ts === undefined) { - // we need to fill this in with the origin_server_ts from its original event - m.created_ts = myCallMemberEvent!.getTs(); - } - - return m; - }; - - // Filter our any invalid or expired memberships, and also our own - we'll add that back in next - let newMemberships = oldMemberships.filter(filterExpired).filter((m) => m.device_id !== localDeviceId); - - // Fix up any memberships that need their created_ts adding - newMemberships = newMemberships.map(transformMemberships); - - // If we're joined, add our own - if (this.isJoined()) { - newMemberships.push(this.makeMyMembershipLegacy(localDeviceId, myPrevMembership)); - } - - return { memberships: newMemberships }; - } private triggerCallMembershipEventUpdate = async (): Promise => { // TODO: Should this await on a shared promise? @@ -1081,64 +953,14 @@ export class MatrixRTCSession extends TypedEventEmitter m.device_id === localDeviceId); - try { - if ( - myCallMemberEvent && - myPrevMembershipData && - isLegacyCallMembershipData(myPrevMembershipData) && - myPrevMembershipData.membershipID === this.membershipId - ) { - myPrevMembership = new CallMembership(myCallMemberEvent, myPrevMembershipData); - } - } catch (e) { - // This would indicate a bug or something weird if our own call membership - // wasn't valid - logger.warn("Our previous call membership was invalid - this shouldn't happen.", e); - } - if (myPrevMembership) { - logger.debug(`${myPrevMembership.getMsUntilExpiry()} until our membership expires`); - } - if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) { - // nothing to do - reschedule the check again - this.memberEventTimeout = setTimeout( - this.triggerCallMembershipEventUpdate, - this.memberEventCheckPeriod, - ); - return; - } - newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership); - } else { - newContent = this.makeNewMembership(localDeviceId); - } + let newContent: {} | SessionMembershipData = {}; + // TODO: implement expiry logic to MSC4143 events + // previously we checked here if the event is timed out and scheduled a check if not. + // maybe there is a better way. + newContent = this.makeNewMembership(localDeviceId); try { - if (legacy) { - await this.client.sendStateEvent( - this.room.roomId, - EventType.GroupCallMemberPrefix, - newContent, - localUserId, - ); - if (this.isJoined()) { - // check periodically to see if we need to refresh our member event - this.memberEventTimeout = setTimeout( - this.triggerCallMembershipEventUpdate, - this.memberEventCheckPeriod, - ); - } - } else if (this.isJoined()) { + if (this.isJoined()) { const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId); const prepareDelayedDisconnection = async (): Promise => { try { @@ -1203,6 +1025,7 @@ export class MatrixRTCSession extends TypedEventEmitter | undefined): boolean { - if (!callMemberEvents?.size) { - return this.useLegacyMemberEvents; - } - - let containsAnyOngoingSession = false; - let containsUnknownOngoingSession = false; - for (const callMemberEvent of callMemberEvents.values()) { - const content = callMemberEvent.getContent(); - if (Array.isArray(content["memberships"])) { - for (const membership of content.memberships) { - if (!new CallMembership(callMemberEvent, membership).isExpired()) { - return true; - } - } - } else if (Object.keys(content).length > 0) { - containsAnyOngoingSession ||= true; - containsUnknownOngoingSession ||= !("focus_active" in content); - } - } - return containsAnyOngoingSession && !containsUnknownOngoingSession ? false : this.useLegacyMemberEvents; - } - private makeMembershipStateKey(localUserId: string, localDeviceId: string): string { const stateKey = `${localUserId}_${localDeviceId}`; if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) { diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 0d2538538f4..b4ecac79a3d 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -35,7 +35,6 @@ import { import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer.ts"; import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter.ts"; import { KnownMembership } from "../@types/membership.ts"; -import { CallMembershipData } from "../matrixrtc/CallMembership.ts"; export enum GroupCallIntent { Ring = "m.ring", @@ -198,11 +197,6 @@ export interface IGroupCallRoomMemberState { "m.calls": IGroupCallRoomMemberCallState[]; } -// XXX: this hasn't made it into the MSC yet -export interface ExperimentalGroupCallRoomMemberState { - memberships: CallMembershipData[]; -} - export enum GroupCallState { LocalCallFeedUninitialized = "local_call_feed_uninitialized", InitializingLocalCallFeed = "initializing_local_call_feed", From 6f743bfa1f7041fe2ab46cebce48602f1951045e Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:14:09 +0100 Subject: [PATCH 38/55] MatrixRTC: Implement expiry logic for CallMembership and additional test coverage (#4587) * remove all legacy call related code and adjust tests. We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests. * dont adjust tests but remove legacy tests * Remove deprecated CallMembership.getLocalExpiry() * Remove references to legacy in test case names * Clean up SessionMembershipData tsdoc * Remove CallMembership.expires * Use correct expire duration. * make expiration methods not return optional values and update docstring * add docs to `SessionMembershipData` * Add new tests for session type member events that before only existed for legacy member events. This reverts commit 795a3cffb61d672941c49e8139eb1d7b15c87d73. * remove code we do not need yet. * Cleanup --------- Co-authored-by: Hugh Nimmo-Smith --- spec/unit/matrixrtc/CallMembership.spec.ts | 62 ++++--- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 165 +++++++++++++++---- src/matrixrtc/CallMembership.ts | 29 ++-- src/matrixrtc/MatrixRTCSession.ts | 4 +- 4 files changed, 188 insertions(+), 72 deletions(-) diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts index 52e6682e592..e330ef1d8fd 100644 --- a/spec/unit/matrixrtc/CallMembership.spec.ts +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -15,7 +15,8 @@ limitations under the License. */ import { MatrixEvent } from "../../../src"; -import { CallMembership, SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { CallMembership, SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership"; +import { membershipTemplate } from "./mocks"; function makeMockEvent(originTs = 0): MatrixEvent { return { @@ -74,6 +75,18 @@ describe("CallMembership", () => { expect(membership.createdTs()).toEqual(67890); }); + it("considers memberships unexpired if local age low enough", () => { + const fakeEvent = makeMockEvent(1000); + fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1)); + expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false); + }); + + it("considers memberships expired if local age large enough", () => { + const fakeEvent = makeMockEvent(1000); + fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1)); + expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true); + }); + it("returns preferred foci", () => { const fakeEvent = makeMockEvent(); const mockFocus = { type: "this_is_a_mock_focus" }; @@ -85,29 +98,26 @@ describe("CallMembership", () => { }); }); - // TODO: re-enable this test when expiry is implemented - // eslint-disable-next-line jest/no-commented-out-tests - // describe("expiry calculation", () => { - // let fakeEvent: MatrixEvent; - // let membership: CallMembership; - - // beforeEach(() => { - // // server origin timestamp for this event is 1000 - // fakeEvent = makeMockEvent(1000); - // membership = new CallMembership(fakeEvent!, membershipTemplate); - - // jest.useFakeTimers(); - // }); - - // afterEach(() => { - // jest.useRealTimers(); - // }); - - // eslint-disable-next-line jest/no-commented-out-tests - // it("calculates time until expiry", () => { - // jest.setSystemTime(2000); - // // should be using absolute expiry time - // expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000); - // }); - // }); + describe("expiry calculation", () => { + let fakeEvent: MatrixEvent; + let membership: CallMembership; + + beforeEach(() => { + // server origin timestamp for this event is 1000 + fakeEvent = makeMockEvent(1000); + membership = new CallMembership(fakeEvent!, membershipTemplate); + + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("calculates time until expiry", () => { + jest.setSystemTime(2000); + // should be using absolute expiry time + expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000); + }); + }); }); diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index d3755744108..4bcd23ae8d4 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src"; import { KnownMembership } from "../../../src/@types/membership"; -import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; import { randomString } from "../../../src/randomstring"; @@ -57,21 +57,19 @@ describe("MatrixRTCSession", () => { expect(sess?.callId).toEqual(""); }); - // TODO: re-enable this test when expiry is implemented - // eslint-disable-next-line jest/no-commented-out-tests - // it("ignores expired memberships events", () => { - // jest.useFakeTimers(); - // const expiredMembership = Object.assign({}, membershipTemplate); - // expiredMembership.expires = 1000; - // expiredMembership.device_id = "EXPIRED"; - // const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]); - - // jest.advanceTimersByTime(2000); - // sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - // expect(sess?.memberships.length).toEqual(1); - // expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); - // jest.useRealTimers(); - // }); + it("ignores expired memberships events", () => { + jest.useFakeTimers(); + const expiredMembership = Object.assign({}, membershipTemplate); + expiredMembership.expires = 1000; + expiredMembership.device_id = "EXPIRED"; + const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]); + + jest.advanceTimersByTime(2000); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess?.memberships.length).toEqual(1); + expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); + jest.useRealTimers(); + }); it("ignores memberships events of members not in the room", () => { const mockRoom = makeMockRoom(membershipTemplate); @@ -80,19 +78,17 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships.length).toEqual(0); }); - // TODO: re-enable this test when expiry is implemented - // eslint-disable-next-line jest/no-commented-out-tests - // it("honours created_ts", () => { - // jest.useFakeTimers(); - // jest.setSystemTime(500); - // const expiredMembership = Object.assign({}, membershipTemplate); - // expiredMembership.created_ts = 500; - // expiredMembership.expires = 1000; - // const mockRoom = makeMockRoom([expiredMembership]); - // sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - // expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); - // jest.useRealTimers(); - // }); + it("honours created_ts", () => { + jest.useFakeTimers(); + jest.setSystemTime(500); + const expiredMembership = Object.assign({}, membershipTemplate); + expiredMembership.created_ts = 500; + expiredMembership.expires = 1000; + const mockRoom = makeMockRoom([expiredMembership]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); + jest.useRealTimers(); + }); it("returns empty session if no membership events are present", () => { const mockRoom = makeMockRoom([]); @@ -273,6 +269,55 @@ describe("MatrixRTCSession", () => { }); }); + describe("getsActiveFocus", () => { + const firstPreferredFocus = { + type: "livekit", + livekit_service_url: "https://active.url", + livekit_alias: "!active:active.url", + }; + it("gets the correct active focus with oldest_membership", () => { + jest.useFakeTimers(); + jest.setSystemTime(3000); + const mockRoom = makeMockRoom([ + Object.assign({}, membershipTemplate, { + device_id: "foo", + created_ts: 500, + foci_preferred: [firstPreferredFocus], + }), + Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), + Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }), + ]); + + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], { + type: "livekit", + focus_selection: "oldest_membership", + }); + expect(sess.getActiveFocus()).toBe(firstPreferredFocus); + jest.useRealTimers(); + }); + it("does not provide focus if the selection method is unknown", () => { + const mockRoom = makeMockRoom([ + Object.assign({}, membershipTemplate, { + device_id: "foo", + created_ts: 500, + foci_preferred: [firstPreferredFocus], + }), + Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), + Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }), + ]); + + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], { + type: "livekit", + focus_selection: "unknown", + }); + expect(sess.getActiveFocus()).toBe(undefined); + }); + }); + describe("joining", () => { let mockRoom: Room; let sendStateEventMock: jest.Mock; @@ -323,6 +368,68 @@ describe("MatrixRTCSession", () => { expect(sess!.isJoined()).toEqual(true); }); + it("sends a membership event when joining a call", async () => { + const realSetTimeout = setTimeout; + jest.useFakeTimers(); + sess!.joinRoomSession([mockFocus], mockFocus); + await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]); + expect(client.sendStateEvent).toHaveBeenCalledWith( + mockRoom!.roomId, + EventType.GroupCallMemberPrefix, + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "AAAAAAA", + expires: DEFAULT_EXPIRE_DURATION, + foci_preferred: [mockFocus], + focus_active: { + focus_selection: "oldest_membership", + type: "livekit", + }, + }, + "_@alice:example.org_AAAAAAA", + ); + await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]); + // Because we actually want to send the state + expect(client.sendStateEvent).toHaveBeenCalledTimes(1); + // For checking if the delayed event is still there or got removed while sending the state. + expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1); + // For scheduling the delayed event + expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1); + // This returns no error so we do not check if we reschedule the event again. this is done in another test. + + jest.useRealTimers(); + }); + + it("uses membershipExpiryTimeout from join config", async () => { + const realSetTimeout = setTimeout; + jest.useFakeTimers(); + sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 }); + await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]); + expect(client.sendStateEvent).toHaveBeenCalledWith( + mockRoom!.roomId, + EventType.GroupCallMemberPrefix, + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "AAAAAAA", + expires: 60000, + foci_preferred: [mockFocus], + focus_active: { + focus_selection: "oldest_membership", + type: "livekit", + }, + }, + + "_@alice:example.org_AAAAAAA", + ); + await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]); + expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1); + jest.useRealTimers(); + }); + describe("calls", () => { const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" }; const activeFocus = { type: "livekit", focus_selection: "oldest_membership" }; diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index ec6a2f4d76f..ce8f2ab6502 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -19,6 +19,13 @@ import { deepCompare } from "../utils.ts"; import { Focus } from "./focus.ts"; import { isLivekitFocusActive } from "./LivekitFocus.ts"; +/** + * The default duration in milliseconds that a membership is considered valid for. + * Ordinarily the client responsible for the session will update the membership before it expires. + * We use this duration as the fallback case where stale sessions are present for some reason. + */ +export const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4; + type CallScope = "m.room" | "m.user"; /** @@ -154,32 +161,26 @@ export class CallMembership { * Gets the absolute expiry timestamp of the membership. * @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable */ - public getAbsoluteExpiry(): number | undefined { - // TODO: implement this in a future PR. Something like: + public getAbsoluteExpiry(): number { // TODO: calculate this from the MatrixRTCSession join configuration directly - // return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION); - - return undefined; + return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION); } /** * @returns The number of milliseconds until the membership expires or undefined if applicable */ - public getMsUntilExpiry(): number | undefined { - // TODO: implement this in a future PR. Something like: - // return this.getAbsoluteExpiry() - Date.now(); - - return undefined; + public getMsUntilExpiry(): number { + // Assume that local clock is sufficiently in sync with other clocks in the distributed system. + // We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate. + // The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2 + return this.getAbsoluteExpiry() - Date.now(); } /** * @returns true if the membership has expired, otherwise false */ public isExpired(): boolean { - // TODO: implement this in a future PR. Something like: - // return this.getMsUntilExpiry() <= 0; - - return false; + return this.getMsUntilExpiry() <= 0; } public getPreferredFoci(): Focus[] { diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 8b65771ae15..c31f3a1763e 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -21,7 +21,7 @@ import { Room } from "../models/room.ts"; import { MatrixClient } from "../client.ts"; import { EventType } from "../@types/event.ts"; import { UpdateDelayedEventAction } from "../@types/requests.ts"; -import { CallMembership, SessionMembershipData } from "./CallMembership.ts"; +import { CallMembership, DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "./CallMembership.ts"; import { RoomStateEvent } from "../models/room-state.ts"; import { Focus } from "./focus.ts"; import { secureRandomBase64Url } from "../randomstring.ts"; @@ -33,8 +33,6 @@ import { MatrixEvent } from "../models/event.ts"; import { isLivekitFocusActive } from "./LivekitFocus.ts"; import { sleep } from "../utils.ts"; -const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4; // 4 hours - const logger = rootLogger.getChild("MatrixRTCSession"); const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; From 38816589f59efd2e6e7057ca007c8ebe87e8fc6e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 7 Jan 2025 12:43:09 +0000 Subject: [PATCH 39/55] v36.0.0-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a056339b7f1..3c5376af4b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "35.1.0", + "version": "36.0.0-rc.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=20.0.0" From 75486b72a63989b3716417b002f4cfd9e18680bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:53:53 +0000 Subject: [PATCH 40/55] Update all non-major dependencies (#4605) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 204 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 98 deletions(-) diff --git a/yarn.lock b/yarn.lock index 084f6f0f426..902a13d99f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1882,14 +1882,6 @@ "@typescript-eslint/visitor-keys" "8.18.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz#01f37c147a735cd78f0ff355e033b9457da1f373" - integrity sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw== - dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" - "@typescript-eslint/scope-manager@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz#ebc9a3b399a69a6052f3d88174456dd399ef5905" @@ -1906,6 +1898,14 @@ "@typescript-eslint/types" "8.18.0" "@typescript-eslint/visitor-keys" "8.18.0" +"@typescript-eslint/scope-manager@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz#794cfc8add4f373b9cd6fa32e367e7565a0e231b" + integrity sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q== + dependencies: + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" + "@typescript-eslint/type-utils@8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz#6f0d12cf923b6fd95ae4d877708c0adaad93c471" @@ -1916,11 +1916,6 @@ debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.14.0.tgz#0d33d8d0b08479c424e7d654855fddf2c71e4021" - integrity sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g== - "@typescript-eslint/types@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737" @@ -1931,19 +1926,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3" integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA== -"@typescript-eslint/typescript-estree@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312" - integrity sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ== - dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" +"@typescript-eslint/types@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c" + integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA== "@typescript-eslint/typescript-estree@8.16.0": version "8.16.0" @@ -1973,6 +1959,20 @@ semver "^7.6.0" ts-api-utils "^1.3.0" +"@typescript-eslint/typescript-estree@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz#c1094bb00bc251ac76cf215569ca27236435036b" + integrity sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q== + dependencies: + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.0.0" + "@typescript-eslint/utils@8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.0.tgz#48f67205d42b65d895797bb7349d1be5c39a62f7" @@ -1984,14 +1984,14 @@ "@typescript-eslint/typescript-estree" "8.18.0" "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd" - integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA== + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.19.1.tgz#dd8eabd46b92bf61e573286e1c0ba6bd243a185b" + integrity sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" + "@typescript-eslint/scope-manager" "8.19.1" + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/typescript-estree" "8.19.1" "@typescript-eslint/utils@^8.13.0": version "8.16.0" @@ -2003,14 +2003,6 @@ "@typescript-eslint/types" "8.16.0" "@typescript-eslint/typescript-estree" "8.16.0" -"@typescript-eslint/visitor-keys@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz#2418d5a54669af9658986ade4e6cfb7767d815ad" - integrity sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ== - dependencies: - "@typescript-eslint/types" "8.14.0" - eslint-visitor-keys "^3.4.3" - "@typescript-eslint/visitor-keys@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705" @@ -2027,6 +2019,14 @@ "@typescript-eslint/types" "8.18.0" eslint-visitor-keys "^4.2.0" +"@typescript-eslint/visitor-keys@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz#fce54d7cfa5351a92387d6c0c5be598caee072e0" + integrity sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q== + dependencies: + "@typescript-eslint/types" "8.19.1" + eslint-visitor-keys "^4.2.0" + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -2492,10 +2492,10 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== +chalk@~5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== char-regex@^1.0.2: version "1.0.2" @@ -2746,7 +2746,7 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.6, debug@~4.3.6: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -2760,7 +2760,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.4, debug@^4.3.7: +debug@^4.3.4, debug@^4.3.6, debug@^4.3.7, debug@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -2902,7 +2902,7 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1: +enhanced-resolve@^5.15.0: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== @@ -2910,6 +2910,14 @@ enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.17.1: + version "5.18.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404" + integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -2992,9 +3000,9 @@ es-errors@^1.2.1, es-errors@^1.3.0: integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-module-lexer@^1.5.3: - version "1.5.4" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" - integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + version "1.6.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== es-object-atoms@^1.0.0: version "1.0.0" @@ -3138,16 +3146,16 @@ eslint-plugin-import@^2.26.0: tsconfig-paths "^3.15.0" eslint-plugin-jest@^28.0.0: - version "28.9.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz#19168dfaed124339cd2252c4c4d1ac3688aeb243" - integrity sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ== + version "28.10.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.10.0.tgz#4b35b8abb0f7cfe699bff8d9060270a2ddd770ea" + integrity sha512-hyMWUxkBH99HpXT3p8hc7REbEZK3D+nk8vHXGgpB+XXsi0gO4PxMSP+pjfUzb67GnV9yawV9a53eUmcde1CCZA== dependencies: "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0" eslint-plugin-jsdoc@^50.0.0: - version "50.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.0.tgz#2c6049a40305313174a30212bc360e775b797a0a" - integrity sha512-tCNp4fR79Le3dYTPB0dKEv7yFyvGkUCa+Z3yuTrrNGGOxBlXo9Pn0PEgroOZikUQOGjxoGMVKNjrOHcYEdfszg== + version "50.6.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.1.tgz#791a668fd4b0700a759e9a16a741a6a805f5b95c" + integrity sha512-UWyaYi6iURdSfdVVqvfOs2vdCVz0J40O/z/HTsv2sFjdjmdlUI/qlKLOTmwbPQ2tAfQnE5F9vqx+B+poF71DBQ== dependencies: "@es-joy/jsdoccomment" "~0.49.0" are-docs-informative "^0.0.2" @@ -3438,15 +3446,15 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.4" + micromatch "^4.0.8" fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" @@ -3459,9 +3467,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + version "1.18.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0" + integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw== dependencies: reusify "^1.0.4" @@ -3620,9 +3628,9 @@ get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-east-asian-width@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" - integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" @@ -4602,9 +4610,9 @@ jest@^29.0.0: jest-cli "^29.7.0" jiti@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.1.tgz#4de9766ccbfa941d9b6390d2b159a4b295a52e6b" - integrity sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g== + version "2.4.2" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560" + integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== jju@~1.4.0: version "1.4.0" @@ -4738,9 +4746,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.0.0: - version "5.39.2" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.39.2.tgz#1faacd8d8ef36b509b2f6e396cce85b645abb04e" - integrity sha512-BuvuWRllLWV/r2G4m9ggNH+DZ6gouP/dhtJPXVlMbWNF++w9/EfrF6k2g7YBKCwjzCC+PXmYtpH8S2t8RjnY4Q== + version "5.41.1" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.41.1.tgz#b6e27186d38e6bccd2ef8346294e78d13322f1cd" + integrity sha512-yNpCCe2REU7U3VRvMASnXSEtfEC2HmOoDW9Vp9teQ9FktJYnuagvSZD3xWq8Ru7sPABkmvbC5TVWuMzIaeADNA== dependencies: "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" @@ -4772,10 +4780,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lilconfig@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" - integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== +lilconfig@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== lines-and-columns@^1.1.6: version "1.2.4" @@ -4790,25 +4798,25 @@ linkify-it@^5.0.0: uc.micro "^2.0.0" lint-staged@^15.0.2: - version "15.2.10" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.10.tgz#92ac222f802ba911897dcf23671da5bb80643cd2" - integrity sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg== + version "15.3.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.3.0.tgz#32a0b3f2f2b8825950bd3b9fb093e045353bdfa3" + integrity sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A== dependencies: - chalk "~5.3.0" + chalk "~5.4.1" commander "~12.1.0" - debug "~4.3.6" + debug "~4.4.0" execa "~8.0.1" - lilconfig "~3.1.2" - listr2 "~8.2.4" + lilconfig "~3.1.3" + listr2 "~8.2.5" micromatch "~4.0.8" pidtree "~0.6.0" string-argv "~0.3.2" - yaml "~2.5.0" + yaml "~2.6.1" -listr2@~8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.4.tgz#486b51cbdb41889108cb7e2c90eeb44519f5a77f" - integrity sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g== +listr2@~8.2.5: + version "8.2.5" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.5.tgz#5c9db996e1afeb05db0448196d3d5f64fec2593d" + integrity sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ== dependencies: cli-truncate "^4.0.0" colorette "^2.0.20" @@ -4958,7 +4966,7 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4, micromatch@~4.0.8: +micromatch@^4.0.4, micromatch@^4.0.8, micromatch@~4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -6132,6 +6140,11 @@ ts-api-utils@^1.3.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== +ts-api-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.0.tgz#b9d7d5f7ec9f736f4d0f09758b8607979044a900" + integrity sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ== + ts-node@^10.9.2: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" @@ -6583,16 +6596,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^2.6.1: +yaml@^2.6.1, yaml@~2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== -yaml@~2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" - integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -6627,6 +6635,6 @@ zod-validation-error@^3.0.3: integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ== zod@^3.22.4: - version "3.24.0" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.0.tgz#babb32313f7c5f4a99812feee806d186b4f76bde" - integrity sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w== + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== From fd894309f2435cb99bbaa26f61175d5b593561d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:54:50 +0000 Subject: [PATCH 41/55] Update guibranco/github-status-action-v2 digest to 56cd38c (#4602) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/sonarcloud.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index aea6aec5665..5e739cc1476 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -27,7 +27,7 @@ jobs: steps: # We create the status here and then update it to success/failure in the `report` stage # This provides an easy link to this workflow_run from the PR before Sonarcloud is done. - - uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 + - uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: pending @@ -87,7 +87,7 @@ jobs: revision: ${{ github.event.workflow_run.head_sha }} token: ${{ secrets.SONAR_TOKEN }} - - uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 + - uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61 if: always() with: authToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29393c9352f..94c1b1921ca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -116,7 +116,7 @@ jobs: steps: - name: Skip SonarCloud on merge queues if: env.ENABLE_COVERAGE == 'false' - uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 + uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success From 1e9c119159d6579fb20a394f162adedc0ef6c926 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:55:34 +0000 Subject: [PATCH 42/55] Update dependency @types/node to v18.19.69 (#4603) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 902a13d99f2..074305efa3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1803,9 +1803,9 @@ undici-types "~5.26.4" "@types/node@18": - version "18.19.67" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.67.tgz#77c4b01641a1e3e1509aff7e10d39e4afd5ae06d" - integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== + version "18.19.70" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.70.tgz#5a77508f5568d16fcd3b711c8102d7a430a04df7" + integrity sha512-RE+K0+KZoEpDUbGGctnGdkrLFwi1eYKTlIHNl2Um98mUkGsm1u2Ff6Ltd0e8DktTtC98uy7rSj+hO8t/QuLoVQ== dependencies: undici-types "~5.26.4" From eef964f07d3f10b1903fbdb802d316271a337578 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:16:07 +0000 Subject: [PATCH 43/55] Update dependency @stylistic/eslint-plugin to v2.12.1 (#4606) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 53 ++++------------------------------------------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/yarn.lock b/yarn.lock index 074305efa3b..8ed14784ff4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1637,9 +1637,9 @@ p-map "^4.0.0" "@stylistic/eslint-plugin@^2.9.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz#50d0289f36f7201055b7fa1729fdc1d8c46e93fa" - integrity sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw== + version "2.12.1" + resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.12.1.tgz#e341beb4e4315084d8be20bceeeda7d8a46f079f" + integrity sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ== dependencies: "@typescript-eslint/utils" "^8.13.0" eslint-visitor-keys "^4.2.0" @@ -1882,14 +1882,6 @@ "@typescript-eslint/visitor-keys" "8.18.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz#ebc9a3b399a69a6052f3d88174456dd399ef5905" - integrity sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg== - dependencies: - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" - "@typescript-eslint/scope-manager@8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz#30b040cb4557804a7e2bcc65cf8fdb630c96546f" @@ -1916,11 +1908,6 @@ debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737" - integrity sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ== - "@typescript-eslint/types@8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3" @@ -1931,20 +1918,6 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c" integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA== -"@typescript-eslint/typescript-estree@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz#9d741e56e5b13469b5190e763432ce5551a9300c" - integrity sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw== - dependencies: - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - "@typescript-eslint/typescript-estree@8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz#d8ca785799fbb9c700cdff1a79c046c3e633c7f9" @@ -1983,7 +1956,7 @@ "@typescript-eslint/types" "8.18.0" "@typescript-eslint/typescript-estree" "8.18.0" -"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0": version "8.19.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.19.1.tgz#dd8eabd46b92bf61e573286e1c0ba6bd243a185b" integrity sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA== @@ -1993,24 +1966,6 @@ "@typescript-eslint/types" "8.19.1" "@typescript-eslint/typescript-estree" "8.19.1" -"@typescript-eslint/utils@^8.13.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" - integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/typescript-estree" "8.16.0" - -"@typescript-eslint/visitor-keys@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705" - integrity sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ== - dependencies: - "@typescript-eslint/types" "8.16.0" - eslint-visitor-keys "^4.2.0" - "@typescript-eslint/visitor-keys@8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz#7b6d33534fa808e33a19951907231ad2ea5c36dd" From 2024b070b02d7fd4a033c3acd072cba5d287217d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:39:07 +0000 Subject: [PATCH 44/55] Update typedoc (#4604) * Update typedoc * Make typedoc happier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/http-api/errors.ts | 6 ++-- yarn.lock | 66 +++++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/http-api/errors.ts b/src/http-api/errors.ts index ab02ccfceec..d8475e6ae8c 100644 --- a/src/http-api/errors.ts +++ b/src/http-api/errors.ts @@ -167,9 +167,9 @@ export class MatrixError extends HTTPError { } /** - * @returns The recommended delay in milliseconds to wait before retrying - * the request that triggered {@link error}, or {@link defaultMs} if the - * error was not due to rate-limiting or if no valid delay is recommended. + * @returns The recommended delay in milliseconds to wait before retrying the request. + * @param error - The error to check for a retry delay. + * @param defaultMs - The delay to use if the error was not due to rate-limiting or if no valid delay is recommended. */ export function safeGetRetryAfterMs(error: unknown, defaultMs: number): number { if (!(error instanceof HTTPError) || !error.isRateLimitError()) { diff --git a/yarn.lock b/yarn.lock index 8ed14784ff4..cc3a00179ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1164,13 +1164,13 @@ integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== "@gerrit0/mini-shiki@^1.24.0": - version "1.24.1" - resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.24.1.tgz#60ef10f4e2cfac7a9223e10b88c128438aa44fd8" - integrity sha512-PNP/Gjv3VqU7z7DjRgO3F9Ok5frTKqtpV+LJW1RzMcr2zpRk0ulhEWnbcNGXzPC7BZyWMIHrkfQX2GZRfxrn6Q== + version "1.26.1" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.26.1.tgz#b59884bd6013976ca66dec197492a1387fdbea52" + integrity sha512-gHFUvv9f1fU2Piou/5Y7Sx5moYxcERbC7CXc6rkDLQTUBg5Dgg9L4u29/nHqfoQ3Y9R0h0BcOhd14uOEZIBP7Q== dependencies: - "@shikijs/engine-oniguruma" "^1.24.0" - "@shikijs/types" "^1.24.0" - "@shikijs/vscode-textmate" "^9.3.0" + "@shikijs/engine-oniguruma" "^1.26.1" + "@shikijs/types" "^1.26.1" + "@shikijs/vscode-textmate" "^10.0.1" "@humanwhocodes/config-array@^0.13.0": version "0.13.0" @@ -1582,26 +1582,26 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@shikijs/engine-oniguruma@^1.24.0": - version "1.24.0" - resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz#4e6f49413fbc96dabfa30cb232ca1acf5ca1a446" - integrity sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg== +"@shikijs/engine-oniguruma@^1.26.1": + version "1.26.1" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.26.1.tgz#f9de733e2473e693b3d10bff32bb9761746c1d71" + integrity sha512-F5XuxN1HljLuvfXv7d+mlTkV7XukC1cawdtOo+7pKgPD83CAB1Sf8uHqP3PK0u7njFH0ZhoXE1r+0JzEgAQ+kg== dependencies: - "@shikijs/types" "1.24.0" - "@shikijs/vscode-textmate" "^9.3.0" + "@shikijs/types" "1.26.1" + "@shikijs/vscode-textmate" "^10.0.1" -"@shikijs/types@1.24.0", "@shikijs/types@^1.24.0": - version "1.24.0" - resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.24.0.tgz#a1755b125cb8fb1780a876a0a57242939eafd79f" - integrity sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug== +"@shikijs/types@1.26.1", "@shikijs/types@^1.26.1": + version "1.26.1" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.26.1.tgz#b5ece69e21000f53d65d15ddae33d9ad9c3763ad" + integrity sha512-d4B00TKKAMaHuFYgRf3L0gwtvqpW4hVdVwKcZYbBfAAQXspgkbWqnFfuFl3MDH6gLbsubOcr+prcnsqah3ny7Q== dependencies: - "@shikijs/vscode-textmate" "^9.3.0" + "@shikijs/vscode-textmate" "^10.0.1" "@types/hast" "^3.0.4" -"@shikijs/vscode-textmate@^9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz#b2f1776e488c1d6c2b6cd129bab62f71bbc9c7ab" - integrity sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA== +"@shikijs/vscode-textmate@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz#d06d45b67ac5e9b0088e3f67ebd3f25c6c3d711a" + integrity sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg== "@sinclair/typebox@^0.24.1": version "0.24.51" @@ -6216,14 +6216,14 @@ typed-array-length@^1.0.6: possible-typed-array-names "^1.0.0" typedoc-plugin-coverage@^3.0.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/typedoc-plugin-coverage/-/typedoc-plugin-coverage-3.4.0.tgz#ca0f4a8cb7d9e2718995520a39e16fdbeb1b22ca" - integrity sha512-I8fLeQEERncGn4sUlGZ+B1ehx4L7VRwqa3i6AP+PFfvZK0ToXBGkh9sK7xs8l8FLPXq7Cv0yVy4YCEGgWNzDBw== + version "3.4.1" + resolved "https://registry.yarnpkg.com/typedoc-plugin-coverage/-/typedoc-plugin-coverage-3.4.1.tgz#13b445cecb674845945e218c4560bbd91299af83" + integrity sha512-V23DAwinAMpGMGcL1R1s8Snr26hrjfIdwGf+4jR/65ZdmeAN+yRX0onfx5JlembTQhR6AePQ/parfQNS0AZ64A== typedoc-plugin-mdn-links@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.3.tgz#30d22be00bc7689a98c0b223b6a487ff6f338ec7" - integrity sha512-q18V8nXF4MqMBGABPVodfxmU2VLK+C7RpyKgrEGP1oP3MAdavLM8Hmeh7zUJAZ4ky+zotO5ZXfhgChegmaDWug== + version "4.0.7" + resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.7.tgz#0949bcca5bf615c4dc35abe653ebc30eade61980" + integrity sha512-S9nUdZShdu+8HyWygmxjqyYIviZVdL36OjPWvxuLVMrS21lqxnVYLqInPYHXalmvitVEqWmrJJk8Al0x6d8wEA== typedoc-plugin-missing-exports@^3.0.0: version "3.1.0" @@ -6231,9 +6231,9 @@ typedoc-plugin-missing-exports@^3.0.0: integrity sha512-Sogbaj+qDa21NjB3SlIw4JXSwmcl/WOjwiPNaVEcPhpNG/MiRTtpwV81cT7h1cbu9StpONFPbddYWR0KV/fTWA== typedoc@^0.27.0: - version "0.27.3" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.3.tgz#0fad232181ce0ac7eda27fe78e56a4b863e1fe59" - integrity sha512-oWT7zDS5oIaxYL5yOikBX4cL99CpNAZn6mI24JZQxsYuIHbtguSSwJ7zThuzNNwSE0wqhlfTSd99HgqKu2aQXQ== + version "0.27.6" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.6.tgz#7e8d067bd5386b7908afcb12c9054a83e8bb326b" + integrity sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw== dependencies: "@gerrit0/mini-shiki" "^1.24.0" lunr "^2.3.9" @@ -6552,9 +6552,9 @@ yallist@^3.0.2: integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^2.6.1, yaml@~2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" - integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== + version "2.7.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" + integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== yargs-parser@^21.1.1: version "21.1.1" From 5b85ae491e52a1da4c4e35fa1f069bf40f431d5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:02:30 +0000 Subject: [PATCH 45/55] Update typescript-eslint monorepo to v8.19.0 (#4607) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 103 +++++++++++++++--------------------------------------- 1 file changed, 29 insertions(+), 74 deletions(-) diff --git a/yarn.lock b/yarn.lock index cc3a00179ad..62eeb795ba7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1857,39 +1857,31 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz#0901933326aea4443b81df3f740ca7dfc45c7bea" - integrity sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw== + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz#5f26c0a833b27bcb1aa402b82e76d3b8dda0b247" + integrity sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/type-utils" "8.18.0" - "@typescript-eslint/utils" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" + "@typescript-eslint/scope-manager" "8.19.1" + "@typescript-eslint/type-utils" "8.19.1" + "@typescript-eslint/utils" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" - ts-api-utils "^1.3.0" + ts-api-utils "^2.0.0" "@typescript-eslint/parser@^8.0.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.0.tgz#a1c9456cbb6a089730bf1d3fc47946c5fb5fe67b" - integrity sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q== + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.19.1.tgz#b836fcfe7a704c8c65f5a50e5b0ff8acfca5c21b" + integrity sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw== dependencies: - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/typescript-estree" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" + "@typescript-eslint/scope-manager" "8.19.1" + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/typescript-estree" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz#30b040cb4557804a7e2bcc65cf8fdb630c96546f" - integrity sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw== - dependencies: - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" - "@typescript-eslint/scope-manager@8.19.1": version "8.19.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz#794cfc8add4f373b9cd6fa32e367e7565a0e231b" @@ -1898,40 +1890,21 @@ "@typescript-eslint/types" "8.19.1" "@typescript-eslint/visitor-keys" "8.19.1" -"@typescript-eslint/type-utils@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz#6f0d12cf923b6fd95ae4d877708c0adaad93c471" - integrity sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow== +"@typescript-eslint/type-utils@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz#23710ab52643c19f74601b3f4a076c98f4e159aa" + integrity sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw== dependencies: - "@typescript-eslint/typescript-estree" "8.18.0" - "@typescript-eslint/utils" "8.18.0" + "@typescript-eslint/typescript-estree" "8.19.1" + "@typescript-eslint/utils" "8.19.1" debug "^4.3.4" - ts-api-utils "^1.3.0" - -"@typescript-eslint/types@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3" - integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA== + ts-api-utils "^2.0.0" "@typescript-eslint/types@8.19.1": version "8.19.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c" integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA== -"@typescript-eslint/typescript-estree@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz#d8ca785799fbb9c700cdff1a79c046c3e633c7f9" - integrity sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg== - dependencies: - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - "@typescript-eslint/typescript-estree@8.19.1": version "8.19.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz#c1094bb00bc251ac76cf215569ca27236435036b" @@ -1946,17 +1919,7 @@ semver "^7.6.0" ts-api-utils "^2.0.0" -"@typescript-eslint/utils@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.0.tgz#48f67205d42b65d895797bb7349d1be5c39a62f7" - integrity sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/typescript-estree" "8.18.0" - -"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0": +"@typescript-eslint/utils@8.19.1", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0": version "8.19.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.19.1.tgz#dd8eabd46b92bf61e573286e1c0ba6bd243a185b" integrity sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA== @@ -1966,14 +1929,6 @@ "@typescript-eslint/types" "8.19.1" "@typescript-eslint/typescript-estree" "8.19.1" -"@typescript-eslint/visitor-keys@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz#7b6d33534fa808e33a19951907231ad2ea5c36dd" - integrity sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw== - dependencies: - "@typescript-eslint/types" "8.18.0" - eslint-visitor-keys "^4.2.0" - "@typescript-eslint/visitor-keys@8.19.1": version "8.19.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz#fce54d7cfa5351a92387d6c0c5be598caee072e0" @@ -6090,11 +6045,6 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -ts-api-utils@^1.3.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" - integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== - ts-api-utils@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.0.tgz#b9d7d5f7ec9f736f4d0f09758b8607979044a900" @@ -6551,11 +6501,16 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^2.6.1, yaml@~2.6.1: +yaml@^2.6.1: version "2.7.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== +yaml@~2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" + integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" From 1da26b5cd199650f77bf3f62a60d84b9add5e270 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 10:16:45 +0000 Subject: [PATCH 46/55] Fix issue with sentinels being incorrect on m.room.member events (#4609) * Fix issue with sentinels being incorrect on m.room.member events Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Simplify change Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/unit/event-timeline-set.spec.ts | 26 +++++++++++++++ src/client.ts | 4 +-- src/models/event-timeline.ts | 22 +------------ src/models/event.ts | 49 ++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index 61b30edb36d..352e882130c 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.spec.ts @@ -23,6 +23,7 @@ import { EventTimelineSet, EventType, Filter, + KnownMembership, MatrixClient, MatrixEvent, MatrixEventEvent, @@ -138,6 +139,31 @@ describe("EventTimelineSet", () => { expect(eventsInLiveTimeline.length).toStrictEqual(1); expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent); }); + + it("should set event.target after applying the membership state update", () => { + const eventTimelineSet = room.getUnfilteredTimelineSet(); + + const ev1 = utils.mkMembership({ + room: roomId, + mship: KnownMembership.Invite, + user: "@sender:server", + skey: userA, + event: true, + }); + const ev2 = utils.mkMembership({ + room: roomId, + mship: KnownMembership.Join, + user: userA, + skey: userA, + name: "This is my displayname", + event: true, + }); + + eventTimelineSet.addLiveEvent(ev1, { addToState: true }); + expect(ev1.target?.name).toBe(userA); + eventTimelineSet.addLiveEvent(ev2, { addToState: true }); + expect(ev2.target?.name).toBe("This is my displayname"); + }); }); describe("addEventToTimeline", () => { diff --git a/src/client.ts b/src/client.ts index eea8faa708f..0e49b16091c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7348,10 +7348,8 @@ export class MatrixClient extends TypedEventEmitter void; [MatrixEventEvent.Replaced]: (event: MatrixEvent) => void; [MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void; + [MatrixEventEvent.SentinelUpdated]: () => void; } & Pick; export class MatrixEvent extends TypedEventEmitter { @@ -328,6 +331,7 @@ export class MatrixEvent extends TypedEventEmitter Date: Fri, 10 Jan 2025 11:46:28 +0100 Subject: [PATCH 47/55] MatrixRTC: move MatrixRTCSession logic into LocalMembershipManager (#4608) * split joinConfig - myMembership related properties get moved into its own interface * Add MyMembershipManager * Remove methods and functions that are from MatrixRTCSession (they now live in MyMembershipManager) * Refactor MatrixRTCSession to use myMembershipManager * fix tests * review * get rid of more memberhsip manager usage in tests * review - fix tests using private membershipManager props * fix circular import --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 36 +- src/matrixrtc/MatrixRTCSession.ts | 372 +++---------------- src/matrixrtc/MembershipManager.ts | 336 +++++++++++++++++ 3 files changed, 399 insertions(+), 345 deletions(-) create mode 100644 src/matrixrtc/MembershipManager.ts diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 4bcd23ae8d4..fd843a0e85f 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -16,7 +16,8 @@ limitations under the License. import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src"; import { KnownMembership } from "../../../src/@types/membership"; -import { SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership"; +import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { MembershipManager } from "../../../src/matrixrtc/MembershipManager"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; import { randomString } from "../../../src/randomstring"; @@ -235,14 +236,13 @@ describe("MatrixRTCSession", () => { }); async function testSession(membershipData: SessionMembershipData): Promise { + const makeNewMembershipSpy = jest.spyOn(MembershipManager.prototype as any, "makeNewMembership"); sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData)); - const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership"); - sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig); await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]); - expect(makeNewMembershipMock).toHaveBeenCalledTimes(1); + expect(makeNewMembershipSpy).toHaveBeenCalledTimes(1); await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]); expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1); @@ -422,7 +422,6 @@ describe("MatrixRTCSession", () => { type: "livekit", }, }, - "_@alice:example.org_AAAAAAA", ); await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]); @@ -454,6 +453,7 @@ describe("MatrixRTCSession", () => { }); }); + const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`; // preparing the delayed disconnect should handle ratelimiting const sendDelayedStateAttempt = new Promise((resolve) => { const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" }); @@ -478,24 +478,30 @@ describe("MatrixRTCSession", () => { }); }); + sess!.joinRoomSession([activeFocusConfig], activeFocus, { + membershipServerSideExpiryTimeout: 9000, + }); + // needed to advance the mock timers properly + // depends on myMembershipManager being created const scheduledDelayDisconnection = new Promise((resolve) => { - const originalFn: () => void = (sess as any).scheduleDelayDisconnection; - (sess as any).scheduleDelayDisconnection = jest.fn(() => { - originalFn.call(sess); + const membershipManager = (sess as any).membershipManager; + const originalFn: () => void = membershipManager.scheduleDelayDisconnection; + membershipManager.scheduleDelayDisconnection = jest.fn(() => { + originalFn.call(membershipManager); resolve(); }); }); - sess!.joinRoomSession([activeFocusConfig], activeFocus, { - membershipServerSideExpiryTimeout: 9000, - }); - - expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 9000); await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches - expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 7500); await sendDelayedStateAttempt; + const callProps = (d: number) => { + return [mockRoom!.roomId, { delay: d }, "org.matrix.msc3401.call.member", {}, userStateKey]; + }; + expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(1, ...callProps(9000)); + expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(2, ...callProps(7500)); + jest.advanceTimersByTime(5000); await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches @@ -514,7 +520,7 @@ describe("MatrixRTCSession", () => { foci_preferred: [activeFocusConfig], focus_active: activeFocus, } satisfies SessionMembershipData, - `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`, + userStateKey, ); await sentDelayedState; diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index c31f3a1763e..e8d6e1f4308 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -20,18 +20,16 @@ import { EventTimeline } from "../models/event-timeline.ts"; import { Room } from "../models/room.ts"; import { MatrixClient } from "../client.ts"; import { EventType } from "../@types/event.ts"; -import { UpdateDelayedEventAction } from "../@types/requests.ts"; -import { CallMembership, DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "./CallMembership.ts"; +import { CallMembership } from "./CallMembership.ts"; import { RoomStateEvent } from "../models/room-state.ts"; import { Focus } from "./focus.ts"; import { secureRandomBase64Url } from "../randomstring.ts"; import { EncryptionKeysEventContent } from "./types.ts"; import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; -import { HTTPError, MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts"; +import { MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts"; import { MatrixEvent } from "../models/event.ts"; -import { isLivekitFocusActive } from "./LivekitFocus.ts"; -import { sleep } from "../utils.ts"; +import { MembershipManager } from "./MembershipManager.ts"; const logger = rootLogger.getChild("MatrixRTCSession"); @@ -67,14 +65,7 @@ export type MatrixRTCSessionEventHandlerMap = { ) => void; }; -export interface JoinSessionConfig { - /** - * If true, generate and share a media key for this participant, - * and emit MatrixRTCSessionEvent.EncryptionKeyChanged when - * media keys for other participants become available. - */ - manageMediaKeys?: boolean; - +export interface MembershipConfig { /** * The timeout (in milliseconds) after we joined the call, that our membership should expire * unless we have explicitly updated it. @@ -94,24 +85,38 @@ export interface JoinSessionConfig { callMemberEventRetryDelayMinimum?: number; /** - * The jitter (in milliseconds) which is added to callMemberEventRetryDelayMinimum before retrying - * sending the membership event. e.g. if this is set to 1000, then a random delay of between 0 and 1000 - * milliseconds will be added. + * The timeout (in milliseconds) with which the deleayed leave event on the server is configured. + * After this time the server will set the event to the disconnected stat if it has not received a keep-alive from the client. */ - callMemberEventRetryJitter?: number; + membershipServerSideExpiryTimeout?: number; + /** + * The interval (in milliseconds) in which the client will send membership keep-alives to the server. + */ + membershipKeepAlivePeriod?: number; + + /** + * @deprecated It should be possible to make it stable without this. + */ + callMemberEventRetryJitter?: number; +} +export interface EncryptionConfig { + /** + * If true, generate and share a media key for this participant, + * and emit MatrixRTCSessionEvent.EncryptionKeyChanged when + * media keys for other participants become available. + */ + manageMediaKeys?: boolean; /** * The minimum time (in milliseconds) between each attempt to send encryption key(s). * e.g. if this is set to 1000, then we will send at most one key event every second. */ updateEncryptionKeyThrottle?: number; - /** * The delay (in milliseconds) after a member leaves before we create and publish a new key, because people * tend to leave calls at the same time. */ makeKeyDelay?: number; - /** * The delay (in milliseconds) between creating and sending a new key and starting to encrypt with it. This * gives other a chance to receive the new key to minimise the chance they don't get media they can't decrypt. @@ -119,40 +124,22 @@ export interface JoinSessionConfig { * makeKeyDelay + useKeyDelay */ useKeyDelay?: number; - - /** - * The timeout (in milliseconds) after which the server will consider the membership to have expired if it - * has not received a keep-alive from the client. - */ - membershipServerSideExpiryTimeout?: number; - - /** - * The period (in milliseconds) that the client will send membership keep-alives to the server. - */ - membershipKeepAlivePeriod?: number; } +export type JoinSessionConfig = MembershipConfig & EncryptionConfig; /** * A MatrixRTCSession manages the membership & properties of a MatrixRTC session. * This class doesn't deal with media at all, just membership & properties of a session. */ export class MatrixRTCSession extends TypedEventEmitter { + private membershipManager?: MembershipManager; + // The session Id of the call, this is the call_id of the call Member event. private _callId: string | undefined; - private relativeExpiry: number | undefined; - // undefined means not yet joined private joinConfig?: JoinSessionConfig; - private get membershipExpiryTimeout(): number { - return this.joinConfig?.membershipExpiryTimeout ?? DEFAULT_EXPIRE_DURATION; - } - - private get callMemberEventRetryDelayMinimum(): number { - return this.joinConfig?.callMemberEventRetryDelayMinimum ?? 3_000; - } - private get updateEncryptionKeyThrottle(): number { return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000; } @@ -165,49 +152,16 @@ export class MatrixRTCSession extends TypedEventEmitter; private expiryTimeout?: ReturnType; private keysEventUpdateTimeout?: ReturnType; private makeNewKeyTimeout?: ReturnType; private setNewKeyTimeouts = new Set>(); - // This is a Focus with the specified fields for an ActiveFocus (e.g. LivekitFocusActive for type="livekit") - private ownFocusActive?: Focus; - // This is a Foci array that contains the Focus objects this user is aware of and proposes to use. - private ownFociPreferred?: Focus[]; - - private updateCallMembershipRunning = false; - private needCallMembershipUpdate = false; - private manageMediaKeys = false; // userId:deviceId => array of (key, timestamp) private encryptionKeys = new Map>(); private lastEncryptionKeyUpdateRequest?: number; - private disconnectDelayId: string | undefined; - // We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys // if it looks like a membership has been updated. private lastMembershipFingerprints: Set | undefined; @@ -338,22 +292,19 @@ export class MatrixRTCSession extends TypedEventEmitter { - await this.leaveRoomSession(1000); + await this.membershipManager?.leaveRoomSession(1000); if (this.expiryTimeout) { clearTimeout(this.expiryTimeout); this.expiryTimeout = undefined; } - if (this.memberEventTimeout) { - clearTimeout(this.memberEventTimeout); - this.memberEventTimeout = undefined; - } + this.membershipManager?.stop(); const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS); roomState?.off(RoomStateEvent.Members, this.onMembershipUpdate); } @@ -376,22 +327,21 @@ export class MatrixRTCSession extends TypedEventEmitter + this.getOldestMembership(), + ); } - - this.ownFocusActive = fociActive; - this.ownFociPreferred = fociPreferred; this.joinConfig = joinConfig; - this.relativeExpiry = this.membershipExpiryTimeout; this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys; - - logger.info(`Joining call session in room ${this.room.roomId} with manageMediaKeys=${this.manageMediaKeys}`); + // TODO: it feels wrong to be doing `setJoined()` and then `joinRoomSession()` non-atomically + // A new api between MembershipManager and the session will need to be defined. + this.membershipManager.setJoined(fociPreferred, fociActive); if (joinConfig?.manageMediaKeys) { this.makeNewSenderKey(); this.requestSendCurrentKey(); } - // We don't wait for this, mostly because it may fail and schedule a retry, so this - // function returning doesn't really mean anything at all. - this.triggerCallMembershipEventUpdate(); + this.membershipManager.joinRoomSession(); this.emit(MatrixRTCSessionEvent.JoinStateChanged, true); } @@ -433,35 +383,14 @@ export class MatrixRTCSession extends TypedEventEmitter => { - // TODO: Should this await on a shared promise? - if (this.updateCallMembershipRunning) { - this.needCallMembershipUpdate = true; - return; - } - - this.updateCallMembershipRunning = true; - try { - // if anything triggers an update while the update is running, do another update afterwards - do { - this.needCallMembershipUpdate = false; - await this.updateCallMembershipEvent(); - } while (this.needCallMembershipUpdate); - } finally { - this.updateCallMembershipRunning = false; - } - }; - - private async updateCallMembershipEvent(): Promise { - if (this.memberEventTimeout) { - clearTimeout(this.memberEventTimeout); - this.memberEventTimeout = undefined; - } - - const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS); - if (!roomState) throw new Error("Couldn't get room state for room " + this.room.roomId); - - const localUserId = this.client.getUserId(); - const localDeviceId = this.client.getDeviceId(); - if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!"); - - let newContent: {} | SessionMembershipData = {}; - // TODO: implement expiry logic to MSC4143 events - // previously we checked here if the event is timed out and scheduled a check if not. - // maybe there is a better way. - newContent = this.makeNewMembership(localDeviceId); - - try { - if (this.isJoined()) { - const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId); - const prepareDelayedDisconnection = async (): Promise => { - try { - const res = await resendIfRateLimited(() => - this.client._unstable_sendDelayedStateEvent( - this.room.roomId, - { - delay: this.membershipServerSideExpiryTimeout, - }, - EventType.GroupCallMemberPrefix, - {}, // leave event - stateKey, - ), - ); - this.disconnectDelayId = res.delay_id; - } catch (e) { - if ( - e instanceof MatrixError && - e.errcode === "M_UNKNOWN" && - e.data["org.matrix.msc4140.errcode"] === "M_MAX_DELAY_EXCEEDED" - ) { - const maxDelayAllowed = e.data["org.matrix.msc4140.max_delay"]; - if ( - typeof maxDelayAllowed === "number" && - this.membershipServerSideExpiryTimeout > maxDelayAllowed - ) { - this.membershipServerSideExpiryTimeoutOverride = maxDelayAllowed; - return prepareDelayedDisconnection(); - } - } - logger.error("Failed to prepare delayed disconnection event:", e); - } - }; - await prepareDelayedDisconnection(); - // Send join event _after_ preparing the delayed disconnection event - await resendIfRateLimited(() => - this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey), - ); - // If sending state cancels your own delayed state, prepare another delayed state - // TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state - if (this.disconnectDelayId !== undefined) { - try { - const knownDisconnectDelayId = this.disconnectDelayId; - await resendIfRateLimited(() => - this.client._unstable_updateDelayedEvent( - knownDisconnectDelayId, - UpdateDelayedEventAction.Restart, - ), - ); - } catch (e) { - if (e instanceof MatrixError && e.errcode === "M_NOT_FOUND") { - // If we get a M_NOT_FOUND we prepare a new delayed event. - // In other error cases we do not want to prepare anything since we do not have the guarantee, that the - // future is not still running. - logger.warn("Failed to update delayed disconnection event, prepare it again:", e); - this.disconnectDelayId = undefined; - await prepareDelayedDisconnection(); - } - } - } - if (this.disconnectDelayId !== undefined) { - this.scheduleDelayDisconnection(); - } - } else { - // Not joined - let sentDelayedDisconnect = false; - if (this.disconnectDelayId !== undefined) { - try { - const knownDisconnectDelayId = this.disconnectDelayId; - await resendIfRateLimited(() => - this.client._unstable_updateDelayedEvent( - knownDisconnectDelayId, - UpdateDelayedEventAction.Send, - ), - ); - sentDelayedDisconnect = true; - } catch (e) { - logger.error("Failed to send our delayed disconnection event:", e); - } - this.disconnectDelayId = undefined; - } - if (!sentDelayedDisconnect) { - await resendIfRateLimited(() => - this.client.sendStateEvent( - this.room.roomId, - EventType.GroupCallMemberPrefix, - {}, - this.makeMembershipStateKey(localUserId, localDeviceId), - ), - ); - } - } - logger.info("Sent updated call member event."); - } catch (e) { - const resendDelay = this.callMemberEventRetryDelayMinimum + Math.random() * this.callMemberEventRetryJitter; - logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`); - await sleep(resendDelay); - await this.triggerCallMembershipEventUpdate(); - } - } - - private scheduleDelayDisconnection(): void { - this.memberEventTimeout = setTimeout(this.delayDisconnection, this.membershipKeepAlivePeriod); - } - - private readonly delayDisconnection = async (): Promise => { - try { - const knownDisconnectDelayId = this.disconnectDelayId!; - await resendIfRateLimited(() => - this.client._unstable_updateDelayedEvent(knownDisconnectDelayId, UpdateDelayedEventAction.Restart), - ); - this.scheduleDelayDisconnection(); - } catch (e) { - logger.error("Failed to delay our disconnection event:", e); - } - }; - - private makeMembershipStateKey(localUserId: string, localDeviceId: string): string { - const stateKey = `${localUserId}_${localDeviceId}`; - if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) { - return stateKey; - } else { - return `_${stateKey}`; - } - } - private onRotateKeyTimeout = (): void => { if (!this.manageMediaKeys) return; @@ -1096,31 +836,3 @@ export class MatrixRTCSession extends TypedEventEmitter(func: () => Promise, numRetriesAllowed: number = 1): Promise { - // eslint-disable-next-line no-constant-condition - while (true) { - try { - return await func(); - } catch (e) { - if (numRetriesAllowed > 0 && e instanceof HTTPError && e.isRateLimitError()) { - numRetriesAllowed--; - let resendDelay: number; - const defaultMs = 5000; - try { - resendDelay = e.getRetryAfterMs() ?? defaultMs; - logger.info(`Rate limited by server, retrying in ${resendDelay}ms`); - } catch (e) { - logger.warn( - `Error while retrieving a rate-limit retry delay, retrying after default delay of ${defaultMs}`, - e, - ); - resendDelay = defaultMs; - } - await sleep(resendDelay); - } else { - throw e; - } - } - } -} diff --git a/src/matrixrtc/MembershipManager.ts b/src/matrixrtc/MembershipManager.ts new file mode 100644 index 00000000000..04d46b31782 --- /dev/null +++ b/src/matrixrtc/MembershipManager.ts @@ -0,0 +1,336 @@ +import { EventType } from "../@types/event.ts"; +import { UpdateDelayedEventAction } from "../@types/requests.ts"; +import type { MatrixClient } from "../client.ts"; +import { HTTPError, MatrixError } from "../http-api/errors.ts"; +import { logger } from "../logger.ts"; +import { EventTimeline } from "../models/event-timeline.ts"; +import { Room } from "../models/room.ts"; +import { sleep } from "../utils.ts"; +import { CallMembership, DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "./CallMembership.ts"; +import { Focus } from "./focus.ts"; +import { isLivekitFocusActive } from "./LivekitFocus.ts"; +import { MembershipConfig } from "./MatrixRTCSession.ts"; + +/** + * This internal class is used by the MatrixRTCSession to manage the local user's own membership of the session. + * @internal + */ +export class MembershipManager { + private relativeExpiry: number | undefined; + + public constructor( + private joinConfig: MembershipConfig | undefined, + private room: Room, + private client: MatrixClient, + private getOldestMembership: () => CallMembership | undefined, + ) {} + private memberEventTimeout?: ReturnType; + + /** + * This is a Foci array that contains the Focus objects this user is aware of and proposes to use. + */ + private ownFociPreferred?: Focus[]; + /** + * This is a Focus with the specified fields for an ActiveFocus (e.g. LivekitFocusActive for type="livekit") + */ + private ownFocusActive?: Focus; + + private updateCallMembershipRunning = false; + private needCallMembershipUpdate = false; + /** + * If the server disallows the configured {@link membershipServerSideExpiryTimeout}, + * this stores a delay that the server does allow. + */ + private membershipServerSideExpiryTimeoutOverride?: number; + private disconnectDelayId: string | undefined; + + private get callMemberEventRetryDelayMinimum(): number { + return this.joinConfig?.callMemberEventRetryDelayMinimum ?? 3_000; + } + private get membershipExpiryTimeout(): number { + return this.joinConfig?.membershipExpiryTimeout ?? DEFAULT_EXPIRE_DURATION; + } + private get membershipServerSideExpiryTimeout(): number { + return ( + this.membershipServerSideExpiryTimeoutOverride ?? + this.joinConfig?.membershipServerSideExpiryTimeout ?? + 8_000 + ); + } + + private get membershipKeepAlivePeriod(): number { + return this.joinConfig?.membershipKeepAlivePeriod ?? 5_000; + } + + private get callMemberEventRetryJitter(): number { + return this.joinConfig?.callMemberEventRetryJitter ?? 2_000; + } + public joinRoomSession(): void { + // We don't wait for this, mostly because it may fail and schedule a retry, so this + // function returning doesn't really mean anything at all. + this.triggerCallMembershipEventUpdate(); + } + public setJoined(fociPreferred: Focus[], fociActive?: Focus): void { + this.ownFocusActive = fociActive; + this.ownFociPreferred = fociPreferred; + this.relativeExpiry = this.membershipExpiryTimeout; + } + public setLeft(): void { + this.relativeExpiry = undefined; + this.ownFocusActive = undefined; + } + public async leaveRoomSession(timeout: number | undefined = undefined): Promise { + if (timeout) { + // The sleep promise returns the string 'timeout' and the membership update void + // A success implies that the membership update was quicker then the timeout. + const raceResult = await Promise.race([this.triggerCallMembershipEventUpdate(), sleep(timeout, "timeout")]); + return raceResult !== "timeout"; + } else { + await this.triggerCallMembershipEventUpdate(); + return true; + } + } + public stop(): void { + if (this.memberEventTimeout) { + clearTimeout(this.memberEventTimeout); + this.memberEventTimeout = undefined; + } + } + public triggerCallMembershipEventUpdate = async (): Promise => { + // TODO: Should this await on a shared promise? + if (this.updateCallMembershipRunning) { + this.needCallMembershipUpdate = true; + return; + } + + this.updateCallMembershipRunning = true; + try { + // if anything triggers an update while the update is running, do another update afterwards + do { + this.needCallMembershipUpdate = false; + await this.updateCallMembershipEvent(); + } while (this.needCallMembershipUpdate); + } finally { + this.updateCallMembershipRunning = false; + } + }; + private makeNewMembership(deviceId: string): SessionMembershipData | {} { + // If we're joined, add our own + if (this.isJoined()) { + return this.makeMyMembership(deviceId); + } + return {}; + } + /* + * Returns true if we intend to be participating in the MatrixRTC session. + * This is determined by checking if the relativeExpiry has been set. + */ + public isJoined(): boolean { + return this.relativeExpiry !== undefined; + } + /** + * Constructs our own membership + */ + private makeMyMembership(deviceId: string): SessionMembershipData { + return { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: deviceId, + expires: this.relativeExpiry, + focus_active: { type: "livekit", focus_selection: "oldest_membership" }, + foci_preferred: this.ownFociPreferred ?? [], + }; + } + + public getActiveFocus(): Focus | undefined { + if (this.ownFocusActive && isLivekitFocusActive(this.ownFocusActive)) { + // A livekit active focus + if (this.ownFocusActive.focus_selection === "oldest_membership") { + const oldestMembership = this.getOldestMembership(); + return oldestMembership?.getPreferredFoci()[0]; + } + } else { + // We do not understand the membership format (could be legacy). We default to oldestMembership + // Once there are other methods this is a hard error! + const oldestMembership = this.getOldestMembership(); + return oldestMembership?.getPreferredFoci()[0]; + } + } + public async updateCallMembershipEvent(): Promise { + if (this.memberEventTimeout) { + clearTimeout(this.memberEventTimeout); + this.memberEventTimeout = undefined; + } + + const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS); + if (!roomState) throw new Error("Couldn't get room state for room " + this.room.roomId); + + const localUserId = this.client.getUserId(); + const localDeviceId = this.client.getDeviceId(); + if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!"); + + let newContent: {} | SessionMembershipData = {}; + // TODO: add back expiary logic to non-legacy events + // previously we checked here if the event is timed out and scheduled a check if not. + // maybe there is a better way. + newContent = this.makeNewMembership(localDeviceId); + + try { + if (this.isJoined()) { + const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId); + const prepareDelayedDisconnection = async (): Promise => { + try { + const res = await resendIfRateLimited(() => + this.client._unstable_sendDelayedStateEvent( + this.room.roomId, + { + delay: this.membershipServerSideExpiryTimeout, + }, + EventType.GroupCallMemberPrefix, + {}, // leave event + stateKey, + ), + ); + logger.log("BEFOER:", this.disconnectDelayId); + this.disconnectDelayId = res.delay_id; + logger.log("AFTER:", this.disconnectDelayId); + } catch (e) { + if ( + e instanceof MatrixError && + e.errcode === "M_UNKNOWN" && + e.data["org.matrix.msc4140.errcode"] === "M_MAX_DELAY_EXCEEDED" + ) { + const maxDelayAllowed = e.data["org.matrix.msc4140.max_delay"]; + if ( + typeof maxDelayAllowed === "number" && + this.membershipServerSideExpiryTimeout > maxDelayAllowed + ) { + this.membershipServerSideExpiryTimeoutOverride = maxDelayAllowed; + return prepareDelayedDisconnection(); + } + } + logger.error("Failed to prepare delayed disconnection event:", e); + } + }; + await prepareDelayedDisconnection(); + // Send join event _after_ preparing the delayed disconnection event + await resendIfRateLimited(() => + this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey), + ); + // If sending state cancels your own delayed state, prepare another delayed state + // TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state + if (this.disconnectDelayId !== undefined) { + try { + const knownDisconnectDelayId = this.disconnectDelayId; + await resendIfRateLimited(() => + this.client._unstable_updateDelayedEvent( + knownDisconnectDelayId, + UpdateDelayedEventAction.Restart, + ), + ); + } catch (e) { + if (e instanceof MatrixError && e.errcode === "M_NOT_FOUND") { + // If we get a M_NOT_FOUND we prepare a new delayed event. + // In other error cases we do not want to prepare anything since we do not have the guarantee, that the + // future is not still running. + logger.warn("Failed to update delayed disconnection event, prepare it again:", e); + this.disconnectDelayId = undefined; + await prepareDelayedDisconnection(); + } + } + } + if (this.disconnectDelayId !== undefined) { + this.scheduleDelayDisconnection(); + } + } else { + // Not joined + let sentDelayedDisconnect = false; + if (this.disconnectDelayId !== undefined) { + try { + const knownDisconnectDelayId = this.disconnectDelayId; + await resendIfRateLimited(() => + this.client._unstable_updateDelayedEvent( + knownDisconnectDelayId, + UpdateDelayedEventAction.Send, + ), + ); + sentDelayedDisconnect = true; + } catch (e) { + logger.error("Failed to send our delayed disconnection event:", e); + } + this.disconnectDelayId = undefined; + } + if (!sentDelayedDisconnect) { + await resendIfRateLimited(() => + this.client.sendStateEvent( + this.room.roomId, + EventType.GroupCallMemberPrefix, + {}, + this.makeMembershipStateKey(localUserId, localDeviceId), + ), + ); + } + } + logger.info("Sent updated call member event."); + } catch (e) { + const resendDelay = this.callMemberEventRetryDelayMinimum + Math.random() * this.callMemberEventRetryJitter; + logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`); + await sleep(resendDelay); + await this.triggerCallMembershipEventUpdate(); + } + } + + private scheduleDelayDisconnection(): void { + this.memberEventTimeout = setTimeout(this.delayDisconnection, this.membershipKeepAlivePeriod); + } + + private readonly delayDisconnection = async (): Promise => { + try { + const knownDisconnectDelayId = this.disconnectDelayId!; + await resendIfRateLimited(() => + this.client._unstable_updateDelayedEvent(knownDisconnectDelayId, UpdateDelayedEventAction.Restart), + ); + this.scheduleDelayDisconnection(); + } catch (e) { + logger.error("Failed to delay our disconnection event:", e); + } + }; + + private makeMembershipStateKey(localUserId: string, localDeviceId: string): string { + const stateKey = `${localUserId}_${localDeviceId}`; + if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) { + return stateKey; + } else { + return `_${stateKey}`; + } + } +} + +async function resendIfRateLimited(func: () => Promise, numRetriesAllowed: number = 1): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + return await func(); + } catch (e) { + if (numRetriesAllowed > 0 && e instanceof HTTPError && e.isRateLimitError()) { + numRetriesAllowed--; + let resendDelay: number; + const defaultMs = 5000; + try { + resendDelay = e.getRetryAfterMs() ?? defaultMs; + logger.info(`Rate limited by server, retrying in ${resendDelay}ms`); + } catch (e) { + logger.warn( + `Error while retrieving a rate-limit retry delay, retrying after default delay of ${defaultMs}`, + e, + ); + resendDelay = defaultMs; + } + await sleep(resendDelay); + } else { + throw e; + } + } + } +} From bed4e9579ea395fcc24b08036b995c87f86e95ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Jan 2025 09:49:15 +0000 Subject: [PATCH 48/55] Mark MSC3981 as stable in Matrix 1.10 (#4023) So we will send the 'recurse' parameter unprefixed for servers that support 1.10 (when it's released). --- src/feature.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/feature.ts b/src/feature.ts index fc097676398..d189a74f306 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -60,6 +60,7 @@ const featureSupportResolver: Record = { }, [Feature.RelationsRecursion]: { unstablePrefixes: ["org.matrix.msc3981"], + matrixVersion: "v1.10", }, [Feature.IntentionalMentions]: { unstablePrefixes: ["org.matrix.msc3952_intentional_mentions"], From ffb228bf5a404c26e03fcea51a207c51f104a716 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:20:54 +0100 Subject: [PATCH 49/55] MatrixRTC: refactor MatrixRTCSession MemberManager API (#4610) * update join and leave internal api. * rename onMembershipUpdate and triggerCallMembershipEventUpdate to onMembershipsUpdate This makes it more clear that we do not talk about our own membership but all memberships in the session * cleanup MembershipManager - add comments and interface how to test this class. - sort methods by public/private - make triggerCallMembershipEventUpdate private * docstrings for getFocusInUse and getActiveFocus * simplify tests and make them only use MembershipManagerInterface methods. This allows to exchange the membershipManager with a different implementation. * convert interface to abstract class. * review (implement interface, make interface internal, dont change public api.) * Make the interface an actual interface. The actual constructor of the class now contains the `Pick` to define what it needs from the client. * move update condition into MembershipManager * renaming public api * Update src/matrixrtc/MatrixRTCSession.ts Co-authored-by: Hugh Nimmo-Smith * Update src/matrixrtc/MatrixRTCSession.ts Co-authored-by: Hugh Nimmo-Smith --------- Co-authored-by: Hugh Nimmo-Smith --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 56 ++++--- src/matrixrtc/MatrixRTCSession.ts | 71 ++++++--- src/matrixrtc/MatrixRTCSessionManager.ts | 2 +- src/matrixrtc/MembershipManager.ts | 145 +++++++++++++------ 4 files changed, 174 insertions(+), 100 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index fd843a0e85f..7e50d952230 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -17,10 +17,10 @@ limitations under the License. import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src"; import { KnownMembership } from "../../../src/@types/membership"; import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; -import { MembershipManager } from "../../../src/matrixrtc/MembershipManager"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; import { randomString } from "../../../src/randomstring"; +import { flushPromises } from "../../test-utils/flushPromises"; import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks"; const mockFocus = { type: "mock" }; @@ -236,16 +236,15 @@ describe("MatrixRTCSession", () => { }); async function testSession(membershipData: SessionMembershipData): Promise { - const makeNewMembershipSpy = jest.spyOn(MembershipManager.prototype as any, "makeNewMembership"); sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData)); sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig); await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]); - expect(makeNewMembershipSpy).toHaveBeenCalledTimes(1); + expect(sendStateEventMock).toHaveBeenCalledTimes(1); await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]); - expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1); + expect(sendDelayedStateMock).toHaveBeenCalledTimes(1); } it("sends events", async () => { @@ -323,9 +322,11 @@ describe("MatrixRTCSession", () => { let sendStateEventMock: jest.Mock; let sendDelayedStateMock: jest.Mock; let sendEventMock: jest.Mock; + let updateDelayedEventMock: jest.Mock; let sentStateEvent: Promise; let sentDelayedState: Promise; + let updatedDelayedEvent: Promise; beforeEach(() => { sentStateEvent = new Promise((resolve) => { @@ -339,12 +340,15 @@ describe("MatrixRTCSession", () => { }; }); }); + updatedDelayedEvent = new Promise((r) => { + updateDelayedEventMock = jest.fn(r); + }); sendEventMock = jest.fn(); client.sendStateEvent = sendStateEventMock; client._unstable_sendDelayedStateEvent = sendDelayedStateMock; client.sendEvent = sendEventMock; - client._unstable_updateDelayedEvent = jest.fn(); + client._unstable_updateDelayedEvent = updateDelayedEventMock; mockRoom = makeMockRoom([]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); @@ -482,19 +486,7 @@ describe("MatrixRTCSession", () => { membershipServerSideExpiryTimeout: 9000, }); - // needed to advance the mock timers properly - // depends on myMembershipManager being created - const scheduledDelayDisconnection = new Promise((resolve) => { - const membershipManager = (sess as any).membershipManager; - const originalFn: () => void = membershipManager.scheduleDelayDisconnection; - membershipManager.scheduleDelayDisconnection = jest.fn(() => { - originalFn.call(membershipManager); - resolve(); - }); - }); - await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches - await sendDelayedStateAttempt; const callProps = (d: number) => { return [mockRoom!.roomId, { delay: d }, "org.matrix.msc3401.call.member", {}, userStateKey]; @@ -525,11 +517,13 @@ describe("MatrixRTCSession", () => { await sentDelayedState; // should have prepared the heartbeat to keep delaying the leave event while still connected - await scheduledDelayDisconnection; - // should have tried updating the delayed leave to test that it wasn't replaced by own state + await updatedDelayedEvent; expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1); - // should update delayed disconnect + + // ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers. + await flushPromises(); jest.advanceTimersByTime(5000); + // should update delayed disconnect expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2); jest.useRealTimers(); @@ -561,7 +555,7 @@ describe("MatrixRTCSession", () => { const onMembershipsChanged = jest.fn(); sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); expect(onMembershipsChanged).not.toHaveBeenCalled(); }); @@ -574,7 +568,7 @@ describe("MatrixRTCSession", () => { sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); mockRoom.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState([], mockRoom.roomId)); - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); expect(onMembershipsChanged).toHaveBeenCalled(); }); @@ -763,7 +757,7 @@ describe("MatrixRTCSession", () => { mockRoom.getLiveTimeline().getState = jest .fn() .mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId)); - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); // member2 re-joins which should trigger an immediate re-send const keysSentPromise2 = new Promise((resolve) => { @@ -772,7 +766,7 @@ describe("MatrixRTCSession", () => { mockRoom.getLiveTimeline().getState = jest .fn() .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId)); - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); // but, that immediate resend is throttled so we need to wait a bit jest.advanceTimersByTime(1000); const { keys } = await keysSentPromise2; @@ -825,7 +819,7 @@ describe("MatrixRTCSession", () => { mockRoom.getLiveTimeline().getState = jest .fn() .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId)); - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); await keysSentPromise2; @@ -879,7 +873,7 @@ describe("MatrixRTCSession", () => { sendEventMock.mockClear(); // these should be a no-op: - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); expect(sendEventMock).toHaveBeenCalledTimes(0); expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1); } finally { @@ -933,7 +927,7 @@ describe("MatrixRTCSession", () => { sendEventMock.mockClear(); // this should be a no-op: - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); expect(sendEventMock).toHaveBeenCalledTimes(0); // advance time to avoid key throttling @@ -947,7 +941,7 @@ describe("MatrixRTCSession", () => { }); // this should re-send the key - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); await keysSentPromise2; @@ -1010,7 +1004,7 @@ describe("MatrixRTCSession", () => { mockRoom.getLiveTimeline().getState = jest .fn() .mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId)); - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); jest.advanceTimersByTime(10000); @@ -1055,7 +1049,7 @@ describe("MatrixRTCSession", () => { ); } - sess!.onMembershipUpdate(); + sess!.onRTCSessionMemberUpdate(); // advance time to avoid key throttling jest.advanceTimersByTime(10000); @@ -1096,7 +1090,7 @@ describe("MatrixRTCSession", () => { mockRoom.getLiveTimeline().getState = jest .fn() .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId)); - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); await new Promise((resolve) => { realSetTimeout(resolve); diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index e8d6e1f4308..7f3665f3eb8 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -29,7 +29,7 @@ import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; import { MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts"; import { MatrixEvent } from "../models/event.ts"; -import { MembershipManager } from "./MembershipManager.ts"; +import { LegacyMembershipManager, IMembershipManager } from "./MembershipManager.ts"; const logger = rootLogger.getChild("MatrixRTCSession"); @@ -132,7 +132,7 @@ export type JoinSessionConfig = MembershipConfig & EncryptionConfig; * This class doesn't deal with media at all, just membership & properties of a session. */ export class MatrixRTCSession extends TypedEventEmitter { - private membershipManager?: MembershipManager; + private membershipManager?: IMembershipManager; // The session Id of the call, this is the call_id of the call Member event. private _callId: string | undefined; @@ -283,7 +283,8 @@ export class MatrixRTCSession extends TypedEventEmitter { - await this.membershipManager?.leaveRoomSession(1000); + await this.membershipManager?.leave(1000); if (this.expiryTimeout) { clearTimeout(this.expiryTimeout); this.expiryTimeout = undefined; } - this.membershipManager?.stop(); const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS); - roomState?.off(RoomStateEvent.Members, this.onMembershipUpdate); + roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate); } /** @@ -324,24 +324,21 @@ export class MatrixRTCSession extends TypedEventEmitter + this.membershipManager = new LegacyMembershipManager(joinConfig, this.room, this.client, () => this.getOldestMembership(), ); } - this.joinConfig = joinConfig; + this.membershipManager!.join(fociPreferred, fociActive); this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys; - // TODO: it feels wrong to be doing `setJoined()` and then `joinRoomSession()` non-atomically - // A new api between MembershipManager and the session will need to be defined. - this.membershipManager.setJoined(fociPreferred, fociActive); if (joinConfig?.manageMediaKeys) { this.makeNewSenderKey(); this.requestSendCurrentKey(); } - this.membershipManager.joinRoomSession(); this.emit(MatrixRTCSessionEvent.JoinStateChanged, true); } @@ -383,12 +380,17 @@ export class MatrixRTCSession extends TypedEventEmitter { + this.recalculateSessionMembers(); + }; + + /** + * Call this when the Matrix room members have changed. + */ + public onRoomMemberUpdate = (): void => { + this.recalculateSessionMembers(); + }; + + /** + * Call this when something changed that may impacts the current MatrixRTC members in this session. + */ + public onRTCSessionMemberUpdate = (): void => { + this.recalculateSessionMembers(); + }; + + /** + * Call this when anything that could impact rtc memberships has changed: Room Members or RTC members. + * * Examines the latest call memberships and handles any encryption key sending or rotation that is needed. * * This function should be called when the room members or call memberships might have changed. */ - public onMembershipUpdate = (): void => { + private recalculateSessionMembers = (): void => { const oldMemberships = this.memberships; this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room); @@ -764,11 +797,7 @@ export class MatrixRTCSession extends TypedEventEmitter 0 && !isNewSession; - sess.onMembershipUpdate(); + sess.onRTCSessionMemberUpdate(); const nowActive = sess.memberships.length > 0; diff --git a/src/matrixrtc/MembershipManager.ts b/src/matrixrtc/MembershipManager.ts index 04d46b31782..e1959171567 100644 --- a/src/matrixrtc/MembershipManager.ts +++ b/src/matrixrtc/MembershipManager.ts @@ -10,20 +10,43 @@ import { CallMembership, DEFAULT_EXPIRE_DURATION, SessionMembershipData } from " import { Focus } from "./focus.ts"; import { isLivekitFocusActive } from "./LivekitFocus.ts"; import { MembershipConfig } from "./MatrixRTCSession.ts"; +/** + * This interface defines what a MembershipManager uses and exposes. + * This interface is what we use to write tests and allows to change the actual implementation + * Without breaking tests because of some internal method renaming. + * + * @internal + */ +export interface IMembershipManager { + isJoined(): boolean; + join(fociPreferred: Focus[], fociActive?: Focus): void; + leave(timeout: number | undefined): Promise; + /** + * call this if the MatrixRTC session members have changed + */ + onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise; + getActiveFocus(): Focus | undefined; +} /** - * This internal class is used by the MatrixRTCSession to manage the local user's own membership of the session. + * This internal class is used by the MatrixRTCSession to manage the local user's own membership of the session. + * + * Its responsibitiy is to manage the locals user membership: + * - send that sate event + * - send the delayed leave event + * - update the delayed leave event while connected + * - update the state event when it times out (for calls longer than membershipExpiryTimeout ~ 4h) + * + * It is possible to test this class on its own. The api surface (to use for tests) is + * defined in `MembershipManagerInterface`. + * + * It is recommended to only use this interface for testing to allow replacing this class. + * * @internal */ -export class MembershipManager { +export class LegacyMembershipManager implements IMembershipManager { private relativeExpiry: number | undefined; - public constructor( - private joinConfig: MembershipConfig | undefined, - private room: Room, - private client: MatrixClient, - private getOldestMembership: () => CallMembership | undefined, - ) {} private memberEventTimeout?: ReturnType; /** @@ -57,29 +80,53 @@ export class MembershipManager { 8_000 ); } - private get membershipKeepAlivePeriod(): number { return this.joinConfig?.membershipKeepAlivePeriod ?? 5_000; } - private get callMemberEventRetryJitter(): number { return this.joinConfig?.callMemberEventRetryJitter ?? 2_000; } - public joinRoomSession(): void { - // We don't wait for this, mostly because it may fail and schedule a retry, so this - // function returning doesn't really mean anything at all. - this.triggerCallMembershipEventUpdate(); + + public constructor( + private joinConfig: MembershipConfig | undefined, + private room: Pick, + private client: Pick< + MatrixClient, + | "getUserId" + | "getDeviceId" + | "sendStateEvent" + | "_unstable_sendDelayedEvent" + | "_unstable_sendDelayedStateEvent" + | "_unstable_updateDelayedEvent" + >, + private getOldestMembership: () => CallMembership | undefined, + ) {} + + /* + * Returns true if we intend to be participating in the MatrixRTC session. + * This is determined by checking if the relativeExpiry has been set. + */ + public isJoined(): boolean { + return this.relativeExpiry !== undefined; } - public setJoined(fociPreferred: Focus[], fociActive?: Focus): void { + + public join(fociPreferred: Focus[], fociActive?: Focus): void { this.ownFocusActive = fociActive; this.ownFociPreferred = fociPreferred; this.relativeExpiry = this.membershipExpiryTimeout; + // We don't wait for this, mostly because it may fail and schedule a retry, so this + // function returning doesn't really mean anything at all. + this.triggerCallMembershipEventUpdate(); } - public setLeft(): void { + + public async leave(timeout: number | undefined = undefined): Promise { this.relativeExpiry = undefined; this.ownFocusActive = undefined; - } - public async leaveRoomSession(timeout: number | undefined = undefined): Promise { + + if (this.memberEventTimeout) { + clearTimeout(this.memberEventTimeout); + this.memberEventTimeout = undefined; + } if (timeout) { // The sleep promise returns the string 'timeout' and the membership update void // A success implies that the membership update was quicker then the timeout. @@ -90,13 +137,38 @@ export class MembershipManager { return true; } } - public stop(): void { - if (this.memberEventTimeout) { - clearTimeout(this.memberEventTimeout); - this.memberEventTimeout = undefined; + + public async onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise { + const isMyMembership = (m: CallMembership): boolean => + m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId(); + + if (this.isJoined() && !memberships.some(isMyMembership)) { + logger.warn("Missing own membership: force re-join"); + // TODO: Should this be awaited? And is there anything to tell the focus? + return this.triggerCallMembershipEventUpdate(); } } - public triggerCallMembershipEventUpdate = async (): Promise => { + + public getActiveFocus(): Focus | undefined { + if (this.ownFocusActive) { + // A livekit active focus + if (isLivekitFocusActive(this.ownFocusActive)) { + if (this.ownFocusActive.focus_selection === "oldest_membership") { + const oldestMembership = this.getOldestMembership(); + return oldestMembership?.getPreferredFoci()[0]; + } + } else { + logger.warn("Unknown own ActiveFocus type. This makes it impossible to connect to an SFU."); + } + } else { + // We do not understand the membership format (could be legacy). We default to oldestMembership + // Once there are other methods this is a hard error! + const oldestMembership = this.getOldestMembership(); + return oldestMembership?.getPreferredFoci()[0]; + } + } + + private triggerCallMembershipEventUpdate = async (): Promise => { // TODO: Should this await on a shared promise? if (this.updateCallMembershipRunning) { this.needCallMembershipUpdate = true; @@ -121,13 +193,7 @@ export class MembershipManager { } return {}; } - /* - * Returns true if we intend to be participating in the MatrixRTC session. - * This is determined by checking if the relativeExpiry has been set. - */ - public isJoined(): boolean { - return this.relativeExpiry !== undefined; - } + /** * Constructs our own membership */ @@ -143,21 +209,7 @@ export class MembershipManager { }; } - public getActiveFocus(): Focus | undefined { - if (this.ownFocusActive && isLivekitFocusActive(this.ownFocusActive)) { - // A livekit active focus - if (this.ownFocusActive.focus_selection === "oldest_membership") { - const oldestMembership = this.getOldestMembership(); - return oldestMembership?.getPreferredFoci()[0]; - } - } else { - // We do not understand the membership format (could be legacy). We default to oldestMembership - // Once there are other methods this is a hard error! - const oldestMembership = this.getOldestMembership(); - return oldestMembership?.getPreferredFoci()[0]; - } - } - public async updateCallMembershipEvent(): Promise { + private async updateCallMembershipEvent(): Promise { if (this.memberEventTimeout) { clearTimeout(this.memberEventTimeout); this.memberEventTimeout = undefined; @@ -192,9 +244,7 @@ export class MembershipManager { stateKey, ), ); - logger.log("BEFOER:", this.disconnectDelayId); this.disconnectDelayId = res.delay_id; - logger.log("AFTER:", this.disconnectDelayId); } catch (e) { if ( e instanceof MatrixError && @@ -213,6 +263,7 @@ export class MembershipManager { logger.error("Failed to prepare delayed disconnection event:", e); } }; + await prepareDelayedDisconnection(); // Send join event _after_ preparing the delayed disconnection event await resendIfRateLimited(() => From f22d5e9d478a90618bcc008a18ffe2f1e9e7f6a7 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:29:50 +0100 Subject: [PATCH 50/55] MatrixRTC: additional TS docs for IMembershipManager interface (#4613) * docstrings for IMembershipManager interface * more details and cleanup * timeout docs --- src/matrixrtc/MembershipManager.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/matrixrtc/MembershipManager.ts b/src/matrixrtc/MembershipManager.ts index e1959171567..47b892d0d66 100644 --- a/src/matrixrtc/MembershipManager.ts +++ b/src/matrixrtc/MembershipManager.ts @@ -18,13 +18,34 @@ import { MembershipConfig } from "./MatrixRTCSession.ts"; * @internal */ export interface IMembershipManager { + /** + * If we are trying to join the session. + * It does not reflect if the room state is already configures to represent us being joined. + * It only means that the Manager is running. + * @returns true if we intend to be participating in the MatrixRTC session + */ isJoined(): boolean; + /** + * Start sending all necessary events to make this user participant in the RTC session. + * @param fociPreferred the list of preferred foci to use in the joined RTC membership event. + * @param fociActive the active focus to use in the joined RTC membership event. + */ join(fociPreferred: Focus[], fociActive?: Focus): void; + /** + * Send all necessary events to make this user leave the RTC session. + * @param timeout the maximum duration in ms until the promise is forced to resolve. + * @returns It resolves with true in case the leave was sent successfully. + * It resolves with false in case we hit the timeout before sending successfully. + */ leave(timeout: number | undefined): Promise; /** - * call this if the MatrixRTC session members have changed + * Call this if the MatrixRTC session members have changed. */ onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise; + /** + * The used active focus in the currently joined session. + * @returns the used active focus in the currently joined session or undefined if not joined. + */ getActiveFocus(): Focus | undefined; } @@ -102,10 +123,6 @@ export class LegacyMembershipManager implements IMembershipManager { private getOldestMembership: () => CallMembership | undefined, ) {} - /* - * Returns true if we intend to be participating in the MatrixRTC session. - * This is determined by checking if the relativeExpiry has been set. - */ public isJoined(): boolean { return this.relativeExpiry !== undefined; } From 9134471dc72a14b29eb207f1c5ef207521f40bd3 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:20:51 +0100 Subject: [PATCH 51/55] MatrixRTC: refactor MatrixRTCSession media encryption key logic into EncryptionManager (#4612) * move Encryption logic from MatrixRTCSession into EncryptionManager * review * review 2 --- src/matrixrtc/EncryptionManager.ts | 500 ++++++++++++++++++++++++++++ src/matrixrtc/MatrixRTCSession.ts | 506 +++-------------------------- 2 files changed, 554 insertions(+), 452 deletions(-) create mode 100644 src/matrixrtc/EncryptionManager.ts diff --git a/src/matrixrtc/EncryptionManager.ts b/src/matrixrtc/EncryptionManager.ts new file mode 100644 index 00000000000..033fc327214 --- /dev/null +++ b/src/matrixrtc/EncryptionManager.ts @@ -0,0 +1,500 @@ +import { type MatrixClient } from "../client.ts"; +import { logger as rootLogger } from "../logger.ts"; +import { MatrixEvent } from "../models/event.ts"; +import { Room } from "../models/room.ts"; +import { EncryptionConfig } from "./MatrixRTCSession.ts"; +import { secureRandomBase64Url } from "../randomstring.ts"; +import { EncryptionKeysEventContent } from "./types.ts"; +import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts"; +import { MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts"; +import { CallMembership } from "./CallMembership.ts"; +import { EventType } from "../@types/event.ts"; +const logger = rootLogger.getChild("MatrixRTCSession"); + +/** + * A type collecting call encryption statistics for a session. + */ +export type Statistics = { + counters: { + /** + * The number of times we have sent a room event containing encryption keys. + */ + roomEventEncryptionKeysSent: number; + /** + * The number of times we have received a room event containing encryption keys. + */ + roomEventEncryptionKeysReceived: number; + }; + totals: { + /** + * The total age (in milliseconds) of all room events containing encryption keys that we have received. + * We track the total age so that we can later calculate the average age of all keys received. + */ + roomEventEncryptionKeysReceivedTotalAge: number; + }; +}; + +/** + * This interface is for testing and for making it possible to interchange the encryption manager. + * @internal + */ +export interface IEncryptionManager { + join(joinConfig: EncryptionConfig | undefined): void; + leave(): void; + onMembershipsUpdate(oldMemberships: CallMembership[]): Promise; + /** + * Process `m.call.encryption_keys` events to track the encryption keys for call participants. + * This should be called each time the relevant event is received from a room timeline. + * If the event is malformed then it will be logged and ignored. + * + * @param event the event to process + */ + onCallEncryptionEventReceived(event: MatrixEvent): void; + getEncryptionKeys(): Map>; + statistics: Statistics; +} + +/** + * This class implements the IEncryptionManager interface, + * and takes care of managing the encryption keys of all rtc members: + * - generate new keys for the local user and send them to other participants + * - track all keys of all other members and update livekit. + * + * @internal + */ +export class EncryptionManager implements IEncryptionManager { + private manageMediaKeys = false; + private keysEventUpdateTimeout?: ReturnType; + private makeNewKeyTimeout?: ReturnType; + private setNewKeyTimeouts = new Set>(); + + private get updateEncryptionKeyThrottle(): number { + return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000; + } + private get makeKeyDelay(): number { + return this.joinConfig?.makeKeyDelay ?? 3_000; + } + private get useKeyDelay(): number { + return this.joinConfig?.useKeyDelay ?? 5_000; + } + + private encryptionKeys = new Map>(); + private lastEncryptionKeyUpdateRequest?: number; + + // We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys + // if it looks like a membership has been updated. + private lastMembershipFingerprints: Set | undefined; + + private currentEncryptionKeyIndex = -1; + + public statistics: Statistics = { + counters: { + roomEventEncryptionKeysSent: 0, + roomEventEncryptionKeysReceived: 0, + }, + totals: { + roomEventEncryptionKeysReceivedTotalAge: 0, + }, + }; + private joinConfig: EncryptionConfig | undefined; + + public constructor( + private client: Pick, + private room: Pick, + private getMemberships: () => CallMembership[], + private onEncryptionKeysChanged: ( + keyBin: Uint8Array, + encryptionKeyIndex: number, + participantId: string, + ) => void, + ) {} + + public getEncryptionKeys(): Map> { + return this.encryptionKeys; + } + private joined = false; + public join(joinConfig: EncryptionConfig): void { + this.joinConfig = joinConfig; + this.joined = true; + this.manageMediaKeys = this.joinConfig?.manageMediaKeys ?? this.manageMediaKeys; + if (this.joinConfig?.manageMediaKeys) { + this.makeNewSenderKey(); + this.requestSendCurrentKey(); + } + } + + public leave(): void { + const userId = this.client.getUserId(); + const deviceId = this.client.getDeviceId(); + + if (!userId) throw new Error("No userId"); + if (!deviceId) throw new Error("No deviceId"); + // clear our encryption keys as we're done with them now (we'll + // make new keys if we rejoin). We leave keys for other participants + // as they may still be using the same ones. + this.encryptionKeys.set(getParticipantId(userId, deviceId), []); + + if (this.makeNewKeyTimeout !== undefined) { + clearTimeout(this.makeNewKeyTimeout); + this.makeNewKeyTimeout = undefined; + } + for (const t of this.setNewKeyTimeouts) { + clearTimeout(t); + } + this.setNewKeyTimeouts.clear(); + + this.manageMediaKeys = false; + this.joined = false; + } + // TODO deduplicate this method. It also is in MatrixRTCSession. + private isMyMembership = (m: CallMembership): boolean => + m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId(); + + public async onMembershipsUpdate(oldMemberships: CallMembership[]): Promise { + if (this.manageMediaKeys && this.joined) { + const oldMembershipIds = new Set( + oldMemberships.filter((m) => !this.isMyMembership(m)).map(getParticipantIdFromMembership), + ); + const newMembershipIds = new Set( + this.getMemberships() + .filter((m) => !this.isMyMembership(m)) + .map(getParticipantIdFromMembership), + ); + + // We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference + // for this once available + const anyLeft = Array.from(oldMembershipIds).some((x) => !newMembershipIds.has(x)); + const anyJoined = Array.from(newMembershipIds).some((x) => !oldMembershipIds.has(x)); + + const oldFingerprints = this.lastMembershipFingerprints; + // always store the fingerprints of these latest memberships + this.storeLastMembershipFingerprints(); + + if (anyLeft) { + if (this.makeNewKeyTimeout) { + // existing rotation in progress, so let it complete + } else { + logger.debug(`Member(s) have left: queueing sender key rotation`); + this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay); + } + } else if (anyJoined) { + logger.debug(`New member(s) have joined: re-sending keys`); + this.requestSendCurrentKey(); + } else if (oldFingerprints) { + // does it look like any of the members have updated their memberships? + const newFingerprints = this.lastMembershipFingerprints!; + + // We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference + // for this once available + const candidateUpdates = + Array.from(oldFingerprints).some((x) => !newFingerprints.has(x)) || + Array.from(newFingerprints).some((x) => !oldFingerprints.has(x)); + if (candidateUpdates) { + logger.debug(`Member(s) have updated/reconnected: re-sending keys to everyone`); + this.requestSendCurrentKey(); + } + } + } + } + + /** + * Generate a new sender key and add it at the next available index + * @param delayBeforeUse - If true, wait for a short period before setting the key for the + * media encryptor to use. If false, set the key immediately. + * @returns The index of the new key + */ + private makeNewSenderKey(delayBeforeUse = false): number { + const userId = this.client.getUserId(); + const deviceId = this.client.getDeviceId(); + + if (!userId) throw new Error("No userId"); + if (!deviceId) throw new Error("No deviceId"); + + const encryptionKey = secureRandomBase64Url(16); + const encryptionKeyIndex = this.getNewEncryptionKeyIndex(); + logger.info("Generated new key at index " + encryptionKeyIndex); + this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, Date.now(), delayBeforeUse); + return encryptionKeyIndex; + } + + /** + * Requests that we resend our current keys to the room. May send a keys event immediately + * or queue for alter if one has already been sent recently. + */ + private requestSendCurrentKey(): void { + if (!this.manageMediaKeys) return; + + if ( + this.lastEncryptionKeyUpdateRequest && + this.lastEncryptionKeyUpdateRequest + this.updateEncryptionKeyThrottle > Date.now() + ) { + logger.info("Last encryption key event sent too recently: postponing"); + if (this.keysEventUpdateTimeout === undefined) { + this.keysEventUpdateTimeout = setTimeout( + this.sendEncryptionKeysEvent, + this.updateEncryptionKeyThrottle, + ); + } + return; + } + + this.sendEncryptionKeysEvent(); + } + + /** + * Get the known encryption keys for a given participant device. + * + * @param userId the user ID of the participant + * @param deviceId the device ID of the participant + * @returns The encryption keys for the given participant, or undefined if they are not known. + */ + private getKeysForParticipant(userId: string, deviceId: string): Array | undefined { + return this.encryptionKeys.get(getParticipantId(userId, deviceId))?.map((entry) => entry.key); + } + + /** + * Re-sends the encryption keys room event + */ + private sendEncryptionKeysEvent = async (indexToSend?: number): Promise => { + if (this.keysEventUpdateTimeout !== undefined) { + clearTimeout(this.keysEventUpdateTimeout); + this.keysEventUpdateTimeout = undefined; + } + this.lastEncryptionKeyUpdateRequest = Date.now(); + + if (!this.joined) return; + + logger.info(`Sending encryption keys event. indexToSend=${indexToSend}`); + + const userId = this.client.getUserId(); + const deviceId = this.client.getDeviceId(); + + if (!userId) throw new Error("No userId"); + if (!deviceId) throw new Error("No deviceId"); + + const myKeys = this.getKeysForParticipant(userId, deviceId); + + if (!myKeys) { + logger.warn("Tried to send encryption keys event but no keys found!"); + return; + } + + if (typeof indexToSend !== "number" && this.currentEncryptionKeyIndex === -1) { + logger.warn("Tried to send encryption keys event but no current key index found!"); + return; + } + + const keyIndexToSend = indexToSend ?? this.currentEncryptionKeyIndex; + const keyToSend = myKeys[keyIndexToSend]; + + try { + const content: EncryptionKeysEventContent = { + keys: [ + { + index: keyIndexToSend, + key: encodeUnpaddedBase64(keyToSend), + }, + ], + device_id: deviceId, + call_id: "", + sent_ts: Date.now(), + }; + + this.statistics.counters.roomEventEncryptionKeysSent += 1; + + await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, content); + + logger.debug( + `Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`, + this.encryptionKeys, + ); + } catch (error) { + const matrixError = error as MatrixError; + if (matrixError.event) { + // cancel the pending event: we'll just generate a new one with our latest + // keys when we resend + this.client.cancelPendingEvent(matrixError.event); + } + if (this.keysEventUpdateTimeout === undefined) { + const resendDelay = safeGetRetryAfterMs(matrixError, 5000); + logger.warn(`Failed to send m.call.encryption_key, retrying in ${resendDelay}`, error); + this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, resendDelay); + } else { + logger.info("Not scheduling key resend as another re-send is already pending"); + } + } + }; + + public onCallEncryptionEventReceived = (event: MatrixEvent): void => { + const userId = event.getSender(); + const content = event.getContent(); + + const deviceId = content["device_id"]; + const callId = content["call_id"]; + + if (!userId) { + logger.warn(`Received m.call.encryption_keys with no userId: callId=${callId}`); + return; + } + + // We currently only handle callId = "" (which is the default for room scoped calls) + if (callId !== "") { + logger.warn( + `Received m.call.encryption_keys with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`, + ); + return; + } + + if (!Array.isArray(content.keys)) { + logger.warn(`Received m.call.encryption_keys where keys wasn't an array: callId=${callId}`); + return; + } + + if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) { + // We store our own sender key in the same set along with keys from others, so it's + // important we don't allow our own keys to be set by one of these events (apart from + // the fact that we don't need it anyway because we already know our own keys). + logger.info("Ignoring our own keys event"); + return; + } + + this.statistics.counters.roomEventEncryptionKeysReceived += 1; + const age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs()); + this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age; + + for (const key of content.keys) { + if (!key) { + logger.info("Ignoring false-y key in keys event"); + continue; + } + + const encryptionKey = key.key; + const encryptionKeyIndex = key.index; + + if ( + !encryptionKey || + encryptionKeyIndex === undefined || + encryptionKeyIndex === null || + callId === undefined || + callId === null || + typeof deviceId !== "string" || + typeof callId !== "string" || + typeof encryptionKey !== "string" || + typeof encryptionKeyIndex !== "number" + ) { + logger.warn( + `Malformed call encryption_key: userId=${userId}, deviceId=${deviceId}, encryptionKeyIndex=${encryptionKeyIndex} callId=${callId}`, + ); + } else { + logger.debug( + `Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`, + this.encryptionKeys, + ); + this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, event.getTs()); + } + } + }; + private storeLastMembershipFingerprints(): void { + this.lastMembershipFingerprints = new Set( + this.getMemberships() + .filter((m) => !this.isMyMembership(m)) + .map((m) => `${getParticipantIdFromMembership(m)}:${m.createdTs()}`), + ); + } + + private getNewEncryptionKeyIndex(): number { + if (this.currentEncryptionKeyIndex === -1) { + return 0; + } + + // maximum key index is 255 + return (this.currentEncryptionKeyIndex + 1) % 256; + } + + /** + * Sets an encryption key at a specified index for a participant. + * The encryption keys for the local participant are also stored here under the + * user and device ID of the local participant. + * If the key is older than the existing key at the index, it will be ignored. + * @param userId - The user ID of the participant + * @param deviceId - Device ID of the participant + * @param encryptionKeyIndex - The index of the key to set + * @param encryptionKeyString - The string representation of the key to set in base64 + * @param timestamp - The timestamp of the key. We assume that these are monotonic for each participant device. + * @param delayBeforeUse - If true, delay before emitting a key changed event. Useful when setting + * encryption keys for the local participant to allow time for the key to + * be distributed. + */ + private setEncryptionKey( + userId: string, + deviceId: string, + encryptionKeyIndex: number, + encryptionKeyString: string, + timestamp: number, + delayBeforeUse = false, + ): void { + const keyBin = decodeBase64(encryptionKeyString); + + const participantId = getParticipantId(userId, deviceId); + if (!this.encryptionKeys.has(participantId)) { + this.encryptionKeys.set(participantId, []); + } + const participantKeys = this.encryptionKeys.get(participantId)!; + + const existingKeyAtIndex = participantKeys[encryptionKeyIndex]; + + if (existingKeyAtIndex) { + if (existingKeyAtIndex.timestamp > timestamp) { + logger.info( + `Ignoring new key at index ${encryptionKeyIndex} for ${participantId} as it is older than existing known key`, + ); + return; + } + + if (keysEqual(existingKeyAtIndex.key, keyBin)) { + existingKeyAtIndex.timestamp = timestamp; + return; + } + } + + participantKeys[encryptionKeyIndex] = { + key: keyBin, + timestamp, + }; + + if (delayBeforeUse) { + const useKeyTimeout = setTimeout(() => { + this.setNewKeyTimeouts.delete(useKeyTimeout); + logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`); + if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) { + this.currentEncryptionKeyIndex = encryptionKeyIndex; + } + this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); + }, this.useKeyDelay); + this.setNewKeyTimeouts.add(useKeyTimeout); + } else { + if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) { + this.currentEncryptionKeyIndex = encryptionKeyIndex; + } + this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId); + } + } + + private onRotateKeyTimeout = (): void => { + if (!this.manageMediaKeys) return; + + this.makeNewKeyTimeout = undefined; + logger.info("Making new sender key for key rotation"); + const newKeyIndex = this.makeNewSenderKey(true); + // send immediately: if we're about to start sending with a new key, it's + // important we get it out to others as soon as we can. + this.sendEncryptionKeysEvent(newKeyIndex); + }; +} + +const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; +function keysEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean { + if (a === b) return true; + return !!a && !!b && a.length === b.length && a.every((x, i) => x === b[i]); +} +const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId); diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 7f3665f3eb8..0540c6207b8 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -23,24 +23,13 @@ import { EventType } from "../@types/event.ts"; import { CallMembership } from "./CallMembership.ts"; import { RoomStateEvent } from "../models/room-state.ts"; import { Focus } from "./focus.ts"; -import { secureRandomBase64Url } from "../randomstring.ts"; -import { EncryptionKeysEventContent } from "./types.ts"; -import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts"; import { KnownMembership } from "../@types/membership.ts"; -import { MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts"; import { MatrixEvent } from "../models/event.ts"; import { LegacyMembershipManager, IMembershipManager } from "./MembershipManager.ts"; +import { EncryptionManager, IEncryptionManager, Statistics } from "./EncryptionManager.ts"; const logger = rootLogger.getChild("MatrixRTCSession"); -const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; -const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId); - -function keysEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean { - if (a === b) return true; - return !!a && !!b && a.length === b.length && a.every((x, i) => x === b[i]); -} - export enum MatrixRTCSessionEvent { // A member joined, left, or updated a property of their membership. MembershipsChanged = "memberships_changed", @@ -133,63 +122,23 @@ export type JoinSessionConfig = MembershipConfig & EncryptionConfig; */ export class MatrixRTCSession extends TypedEventEmitter { private membershipManager?: IMembershipManager; - + private encryptionManager: IEncryptionManager; // The session Id of the call, this is the call_id of the call Member event. private _callId: string | undefined; - // undefined means not yet joined - private joinConfig?: JoinSessionConfig; - - private get updateEncryptionKeyThrottle(): number { - return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000; - } - - private get makeKeyDelay(): number { - return this.joinConfig?.makeKeyDelay ?? 3_000; - } - - private get useKeyDelay(): number { - return this.joinConfig?.useKeyDelay ?? 5_000; - } - + /** + * This timeout is responsible to track any expiration. We need to know when we have to start + * to ignore other call members. There is no callback for this. This timeout will always be configured to + * emit when the next membership expires. + */ private expiryTimeout?: ReturnType; - private keysEventUpdateTimeout?: ReturnType; - private makeNewKeyTimeout?: ReturnType; - private setNewKeyTimeouts = new Set>(); - - private manageMediaKeys = false; - // userId:deviceId => array of (key, timestamp) - private encryptionKeys = new Map>(); - private lastEncryptionKeyUpdateRequest?: number; - - // We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys - // if it looks like a membership has been updated. - private lastMembershipFingerprints: Set | undefined; - - private currentEncryptionKeyIndex = -1; /** * The statistics for this session. */ - public statistics = { - counters: { - /** - * The number of times we have sent a room event containing encryption keys. - */ - roomEventEncryptionKeysSent: 0, - /** - * The number of times we have received a room event containing encryption keys. - */ - roomEventEncryptionKeysReceived: 0, - }, - totals: { - /** - * The total age (in milliseconds) of all room events containing encryption keys that we have received. - * We track the total age so that we can later calculate the average age of all keys received. - */ - roomEventEncryptionKeysReceivedTotalAge: 0, - }, - }; + public get statistics(): Statistics { + return this.encryptionManager.statistics; + } /** * The callId (sessionId) of the call. @@ -200,6 +149,7 @@ export class MatrixRTCSession extends TypedEventEmitter this.memberships, + (keyBin: Uint8Array, encryptionKeyIndex: number, participantId: string) => { + this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId); + }, + ); } /* @@ -324,7 +282,7 @@ export class MatrixRTCSession extends TypedEventEmitter { + this.encryptionManager.getEncryptionKeys().forEach((keys, participantId) => { keys.forEach((key, index) => { this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, key.key, index, participantId); }); }); } - /** - * Get the known encryption keys for a given participant device. - * - * @param userId the user ID of the participant - * @param deviceId the device ID of the participant - * @returns The encryption keys for the given participant, or undefined if they are not known. - * - * @deprecated This will be made private in a future release. - */ - public getKeysForParticipant(userId: string, deviceId: string): Array | undefined { - return this.getKeysForParticipantInternal(userId, deviceId); - } - - private getKeysForParticipantInternal(userId: string, deviceId: string): Array | undefined { - return this.encryptionKeys.get(getParticipantId(userId, deviceId))?.map((entry) => entry.key); - } - /** * A map of keys used to encrypt and decrypt (we are using a symmetric * cipher) given participant's media. This also includes our own key @@ -431,207 +371,15 @@ export class MatrixRTCSession extends TypedEventEmitter]> { + const keys = + this.encryptionManager.getEncryptionKeys() ?? + new Map>(); // the returned array doesn't contain the timestamps - return Array.from(this.encryptionKeys.entries()) + return Array.from(keys.entries()) .map(([participantId, keys]): [string, Uint8Array[]] => [participantId, keys.map((k) => k.key)]) .values(); } - private getNewEncryptionKeyIndex(): number { - if (this.currentEncryptionKeyIndex === -1) { - return 0; - } - - // maximum key index is 255 - return (this.currentEncryptionKeyIndex + 1) % 256; - } - - /** - * Sets an encryption key at a specified index for a participant. - * The encryption keys for the local participant are also stored here under the - * user and device ID of the local participant. - * If the key is older than the existing key at the index, it will be ignored. - * @param userId - The user ID of the participant - * @param deviceId - Device ID of the participant - * @param encryptionKeyIndex - The index of the key to set - * @param encryptionKeyString - The string representation of the key to set in base64 - * @param timestamp - The timestamp of the key. We assume that these are monotonic for each participant device. - * @param delayBeforeUse - If true, delay before emitting a key changed event. Useful when setting - * encryption keys for the local participant to allow time for the key to - * be distributed. - */ - private setEncryptionKey( - userId: string, - deviceId: string, - encryptionKeyIndex: number, - encryptionKeyString: string, - timestamp: number, - delayBeforeUse = false, - ): void { - const keyBin = decodeBase64(encryptionKeyString); - - const participantId = getParticipantId(userId, deviceId); - if (!this.encryptionKeys.has(participantId)) { - this.encryptionKeys.set(participantId, []); - } - const participantKeys = this.encryptionKeys.get(participantId)!; - - const existingKeyAtIndex = participantKeys[encryptionKeyIndex]; - - if (existingKeyAtIndex) { - if (existingKeyAtIndex.timestamp > timestamp) { - logger.info( - `Ignoring new key at index ${encryptionKeyIndex} for ${participantId} as it is older than existing known key`, - ); - return; - } - - if (keysEqual(existingKeyAtIndex.key, keyBin)) { - existingKeyAtIndex.timestamp = timestamp; - return; - } - } - - participantKeys[encryptionKeyIndex] = { - key: keyBin, - timestamp, - }; - - if (delayBeforeUse) { - const useKeyTimeout = setTimeout(() => { - this.setNewKeyTimeouts.delete(useKeyTimeout); - logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`); - if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) { - this.currentEncryptionKeyIndex = encryptionKeyIndex; - } - this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId); - }, this.useKeyDelay); - this.setNewKeyTimeouts.add(useKeyTimeout); - } else { - if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) { - this.currentEncryptionKeyIndex = encryptionKeyIndex; - } - this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId); - } - } - - /** - * Generate a new sender key and add it at the next available index - * @param delayBeforeUse - If true, wait for a short period before setting the key for the - * media encryptor to use. If false, set the key immediately. - * @returns The index of the new key - */ - private makeNewSenderKey(delayBeforeUse = false): number { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - - if (!userId) throw new Error("No userId"); - if (!deviceId) throw new Error("No deviceId"); - - const encryptionKey = secureRandomBase64Url(16); - const encryptionKeyIndex = this.getNewEncryptionKeyIndex(); - logger.info("Generated new key at index " + encryptionKeyIndex); - this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, Date.now(), delayBeforeUse); - return encryptionKeyIndex; - } - - /** - * Requests that we resend our current keys to the room. May send a keys event immediately - * or queue for alter if one has already been sent recently. - */ - private requestSendCurrentKey(): void { - if (!this.manageMediaKeys) return; - - if ( - this.lastEncryptionKeyUpdateRequest && - this.lastEncryptionKeyUpdateRequest + this.updateEncryptionKeyThrottle > Date.now() - ) { - logger.info("Last encryption key event sent too recently: postponing"); - if (this.keysEventUpdateTimeout === undefined) { - this.keysEventUpdateTimeout = setTimeout( - this.sendEncryptionKeysEvent, - this.updateEncryptionKeyThrottle, - ); - } - return; - } - - this.sendEncryptionKeysEvent(); - } - - /** - * Re-sends the encryption keys room event - */ - private sendEncryptionKeysEvent = async (indexToSend?: number): Promise => { - if (this.keysEventUpdateTimeout !== undefined) { - clearTimeout(this.keysEventUpdateTimeout); - this.keysEventUpdateTimeout = undefined; - } - this.lastEncryptionKeyUpdateRequest = Date.now(); - - if (!this.isJoined()) return; - - logger.info(`Sending encryption keys event. indexToSend=${indexToSend}`); - - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - - if (!userId) throw new Error("No userId"); - if (!deviceId) throw new Error("No deviceId"); - - const myKeys = this.getKeysForParticipant(userId, deviceId); - - if (!myKeys) { - logger.warn("Tried to send encryption keys event but no keys found!"); - return; - } - - if (typeof indexToSend !== "number" && this.currentEncryptionKeyIndex === -1) { - logger.warn("Tried to send encryption keys event but no current key index found!"); - return; - } - - const keyIndexToSend = indexToSend ?? this.currentEncryptionKeyIndex; - const keyToSend = myKeys[keyIndexToSend]; - - try { - const content: EncryptionKeysEventContent = { - keys: [ - { - index: keyIndexToSend, - key: encodeUnpaddedBase64(keyToSend), - }, - ], - device_id: deviceId, - call_id: "", - sent_ts: Date.now(), - }; - - this.statistics.counters.roomEventEncryptionKeysSent += 1; - - await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, content); - - logger.debug( - `Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`, - this.encryptionKeys, - ); - } catch (error) { - const matrixError = error as MatrixError; - if (matrixError.event) { - // cancel the pending event: we'll just generate a new one with our latest - // keys when we resend - this.client.cancelPendingEvent(matrixError.event); - } - if (this.keysEventUpdateTimeout === undefined) { - const resendDelay = safeGetRetryAfterMs(matrixError, 5000); - logger.warn(`Failed to send m.call.encryption_key, retrying in ${resendDelay}`, error); - this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, resendDelay); - } else { - logger.info("Not scheduling key resend as another re-send is already pending"); - } - } - }; - /** * Sets a timer for the soonest membership expiry */ @@ -656,24 +404,6 @@ export class MatrixRTCSession extends TypedEventEmitter { - const userId = event.getSender(); - const content = event.getContent(); - - const deviceId = content["device_id"]; - const callId = content["call_id"]; - - if (!userId) { - logger.warn(`Received m.call.encryption_keys with no userId: callId=${callId}`); - return; - } - - // We currently only handle callId = "" (which is the default for room scoped calls) - if (callId !== "") { - logger.warn( - `Received m.call.encryption_keys with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`, - ); - return; - } - - if (!Array.isArray(content.keys)) { - logger.warn(`Received m.call.encryption_keys where keys wasn't an array: callId=${callId}`); - return; - } - - if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) { - // We store our own sender key in the same set along with keys from others, so it's - // important we don't allow our own keys to be set by one of these events (apart from - // the fact that we don't need it anyway because we already know our own keys). - logger.info("Ignoring our own keys event"); - return; - } - - this.statistics.counters.roomEventEncryptionKeysReceived += 1; - const age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs()); - this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age; - - for (const key of content.keys) { - if (!key) { - logger.info("Ignoring false-y key in keys event"); - continue; - } - - const encryptionKey = key.key; - const encryptionKeyIndex = key.index; - - if ( - !encryptionKey || - encryptionKeyIndex === undefined || - encryptionKeyIndex === null || - callId === undefined || - callId === null || - typeof deviceId !== "string" || - typeof callId !== "string" || - typeof encryptionKey !== "string" || - typeof encryptionKeyIndex !== "number" - ) { - logger.warn( - `Malformed call encryption_key: userId=${userId}, deviceId=${deviceId}, encryptionKeyIndex=${encryptionKeyIndex} callId=${callId}`, - ); - } else { - logger.debug( - `Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`, - this.encryptionKeys, - ); - this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, event.getTs()); - } - } + this.encryptionManager.onCallEncryptionEventReceived(event); }; - private isMyMembership = (m: CallMembership): boolean => - m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId(); - /** * @deprecated use onRoomMemberUpdate or onRTCSessionMemberUpdate instead. this should be called when any membership in the call is updated * the old name might have implied to only need to call this when your own membership changes. @@ -799,69 +460,10 @@ export class MatrixRTCSession extends TypedEventEmitter !this.isMyMembership(m)).map(getParticipantIdFromMembership), - ); - const newMembershipIds = new Set( - this.memberships.filter((m) => !this.isMyMembership(m)).map(getParticipantIdFromMembership), - ); - - // We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference - // for this once available - const anyLeft = Array.from(oldMembershipIds).some((x) => !newMembershipIds.has(x)); - const anyJoined = Array.from(newMembershipIds).some((x) => !oldMembershipIds.has(x)); - - const oldFingerprints = this.lastMembershipFingerprints; - // always store the fingerprints of these latest memberships - this.storeLastMembershipFingerprints(); - - if (anyLeft) { - if (this.makeNewKeyTimeout) { - // existing rotation in progress, so let it complete - } else { - logger.debug(`Member(s) have left: queueing sender key rotation`); - this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay); - } - } else if (anyJoined) { - logger.debug(`New member(s) have joined: re-sending keys`); - this.requestSendCurrentKey(); - } else if (oldFingerprints) { - // does it look like any of the members have updated their memberships? - const newFingerprints = this.lastMembershipFingerprints!; - - // We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference - // for this once available - const candidateUpdates = - Array.from(oldFingerprints).some((x) => !newFingerprints.has(x)) || - Array.from(newFingerprints).some((x) => !oldFingerprints.has(x)); - if (candidateUpdates) { - logger.debug(`Member(s) have updated/reconnected: re-sending keys to everyone`); - this.requestSendCurrentKey(); - } - } - } + // This also needs to be done if `changed` = false + // A member might have updated their fingerprint (created_ts) + this.encryptionManager.onMembershipsUpdate(oldMemberships); this.setExpiryTimer(); }; - - private storeLastMembershipFingerprints(): void { - this.lastMembershipFingerprints = new Set( - this.memberships - .filter((m) => !this.isMyMembership(m)) - .map((m) => `${getParticipantIdFromMembership(m)}:${m.createdTs()}`), - ); - } - - private onRotateKeyTimeout = (): void => { - if (!this.manageMediaKeys) return; - - this.makeNewKeyTimeout = undefined; - logger.info("Making new sender key for key rotation"); - const newKeyIndex = this.makeNewSenderKey(true); - // send immediately: if we're about to start sending with a new key, it's - // important we get it out to others as soon as we can. - this.sendEncryptionKeysEvent(newKeyIndex); - }; } From 72ee5504d5ab39dc40c4ba1a5601a90b7a6b3426 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 14 Jan 2025 14:08:54 +0000 Subject: [PATCH 52/55] v36.0.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fedadb80fe9..31edfc64338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +Changes in [36.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.0.0) (2025-01-14) +================================================================================================== +## 🚨 BREAKING CHANGES + +* Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership ([#4583](https://github.com/matrix-org/matrix-js-sdk/pull/4583)). Contributed by @toger5. + +## ✨ Features + +* MatrixRTC: Implement expiry logic for CallMembership and additional test coverage ([#4587](https://github.com/matrix-org/matrix-js-sdk/pull/4587)). Contributed by @toger5. + +## 🐛 Bug Fixes + +* Don't retry on 4xx responses ([#4601](https://github.com/matrix-org/matrix-js-sdk/pull/4601)). Contributed by @dbkr. +* Upgrade matrix-sdk-crypto-wasm to 12.1.0 ([#4596](https://github.com/matrix-org/matrix-js-sdk/pull/4596)). Contributed by @andybalaam. +* Avoid key prompts when resetting crypto ([#4586](https://github.com/matrix-org/matrix-js-sdk/pull/4586)). Contributed by @dbkr. +* Handle when aud OIDC claim is an Array ([#4584](https://github.com/matrix-org/matrix-js-sdk/pull/4584)). Contributed by @liamdiprose. +* Save the key backup key to 4S during `bootstrapSecretStorage ` ([#4542](https://github.com/matrix-org/matrix-js-sdk/pull/4542)). Contributed by @dbkr. +* Only re-prepare MatrixrRTC delayed disconnection event on 404 ([#4575](https://github.com/matrix-org/matrix-js-sdk/pull/4575)). Contributed by @toger5. + + Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18) ================================================================================================== This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage. diff --git a/package.json b/package.json index 3c5376af4b1..619f109d2b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "36.0.0-rc.0", + "version": "36.0.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=20.0.0" From ffbb4716c442856ebe60c15b6f1825e6f4885801 Mon Sep 17 00:00:00 2001 From: m004 <38407173+m004@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:49:18 +0100 Subject: [PATCH 53/55] Add authenticated media to getAvatarURL in room and room-member models (#4616) --- spec/unit/room-member.spec.ts | 34 ++++++++++++++++++++++++++++ spec/unit/room.spec.ts | 42 +++++++++++++++++++++++++++++++++++ src/models/room-member.ts | 17 +++++++++++++- src/models/room.ts | 17 +++++++++++++- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/spec/unit/room-member.spec.ts b/spec/unit/room-member.spec.ts index eb53989d47f..2b0e223f9fa 100644 --- a/spec/unit/room-member.spec.ts +++ b/spec/unit/room-member.spec.ts @@ -65,6 +65,40 @@ describe("RoomMember", function () { const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false); expect(url).toEqual(null); }); + + it("should return unauthenticated media URL if useAuthentication is not set", function () { + member.events.member = utils.mkEvent({ + event: true, + type: "m.room.member", + skey: userA, + room: roomId, + user: userA, + content: { + membership: KnownMembership.Join, + avatar_url: "mxc://flibble/wibble", + }, + }); + const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false); + // Check for unauthenticated media prefix + expect(url?.indexOf("/_matrix/media/v3/")).not.toEqual(-1); + }); + + it("should return authenticated media URL if useAuthentication=true", function () { + member.events.member = utils.mkEvent({ + event: true, + type: "m.room.member", + skey: userA, + room: roomId, + user: userA, + content: { + membership: KnownMembership.Join, + avatar_url: "mxc://flibble/wibble", + }, + }); + const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false, true); + // Check for authenticated media prefix + expect(url?.indexOf("/_matrix/client/v1/media/")).not.toEqual(-1); + }); }); describe("setPowerLevelEvent", function () { diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 1f3efc0bf57..9a31e492f3c 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -299,6 +299,48 @@ describe("Room", function () { const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false); expect(url).toEqual(null); }); + + it("should return unauthenticated media URL if useAuthentication is not set", function () { + // @ts-ignore - mocked doesn't handle overloads sanely + mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) { + if (type === EventType.RoomAvatar && key === "") { + return utils.mkEvent({ + event: true, + type: EventType.RoomAvatar, + skey: "", + room: roomId, + user: userA, + content: { + url: "mxc://flibble/wibble", + }, + }); + } + }); + const url = room.getAvatarUrl(hsUrl, 100, 100, "scale"); + // Check for unauthenticated media prefix + expect(url?.indexOf("/_matrix/media/v3/")).not.toEqual(-1); + }); + + it("should return authenticated media URL if useAuthentication=true", function () { + // @ts-ignore - mocked doesn't handle overloads sanely + mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) { + if (type === EventType.RoomAvatar && key === "") { + return utils.mkEvent({ + event: true, + type: EventType.RoomAvatar, + skey: "", + room: roomId, + user: userA, + content: { + url: "mxc://flibble/wibble", + }, + }); + } + }); + const url = room.getAvatarUrl(hsUrl, 100, 100, "scale", undefined, true); + // Check for authenticated media prefix + expect(url?.indexOf("/_matrix/client/v1/media/")).not.toEqual(-1); + }); }); describe("getMember", function () { diff --git a/src/models/room-member.ts b/src/models/room-member.ts index 0f7e0b8dd62..8f1aea8533c 100644 --- a/src/models/room-member.ts +++ b/src/models/room-member.ts @@ -368,6 +368,11 @@ export class RoomMember extends TypedEventEmitter { * "crop" or "scale". * @param allowDefault - True to allow an identicon for this room if an * avatar URL wasn't explicitly set. Default: true. (Deprecated) + * @param useAuthentication - (optional) If true, the caller supports authenticated + * media and wants an authentication-required URL. Note that server support for + * authenticated media will not be checked - it is the caller's responsibility + * to do so before calling this function. Note also that useAuthentication + * implies allowRedirects. Defaults to false (unauthenticated endpoints). * @returns the avatar URL or null. */ public getAvatarUrl( @@ -1669,6 +1674,7 @@ export class Room extends ReadReceipt { height: number, resizeMethod: ResizeMethod, allowDefault = true, + useAuthentication: boolean = false, ): string | null { const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, ""); if (!roomAvatarEvent && !allowDefault) { @@ -1677,7 +1683,16 @@ export class Room extends ReadReceipt { const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null; if (mainUrl) { - return getHttpUriForMxc(baseUrl, mainUrl, width, height, resizeMethod); + return getHttpUriForMxc( + baseUrl, + mainUrl, + width, + height, + resizeMethod, + undefined, + undefined, + useAuthentication, + ); } return null; From 5babcaf4b34d48bf15775f7fbb5168883a0831f9 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 15 Jan 2025 13:29:02 +0100 Subject: [PATCH 54/55] feat(secret storage): `keyId` in `SecretStorage.setDefaultKeyId` can be set at `null` in order to delete an exising recovery key (#4615) --- spec/unit/secret-storage.spec.ts | 81 +++++++++++++++++++++++++++++++- src/crypto/EncryptionSetup.ts | 5 +- src/secret-storage.ts | 34 ++++++++++---- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/spec/unit/secret-storage.spec.ts b/spec/unit/secret-storage.spec.ts index 6299141829e..9caaa74580e 100644 --- a/spec/unit/secret-storage.spec.ts +++ b/spec/unit/secret-storage.spec.ts @@ -23,12 +23,14 @@ import { SecretStorageCallbacks, SecretStorageKeyDescriptionAesV1, SecretStorageKeyDescriptionCommon, + ServerSideSecretStorage, ServerSideSecretStorageImpl, trimTrailingEquals, } from "../../src/secret-storage"; import { randomString } from "../../src/randomstring"; import { SecretInfo } from "../../src/secret-storage.ts"; -import { AccountDataEvents } from "../../src"; +import { AccountDataEvents, ClientEvent, MatrixEvent, TypedEventEmitter } from "../../src"; +import { defer, IDeferred } from "../../src/utils"; declare module "../../src/@types/event" { interface SecretStorageAccountDataEvents { @@ -273,6 +275,78 @@ describe("ServerSideSecretStorageImpl", function () { expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("unknown algorithm")); }); }); + + describe("setDefaultKeyId", function () { + let secretStorage: ServerSideSecretStorage; + let accountDataAdapter: Mocked; + let accountDataPromise: IDeferred; + beforeEach(() => { + accountDataAdapter = mockAccountDataClient(); + accountDataPromise = defer(); + accountDataAdapter.setAccountData.mockImplementation(() => { + accountDataPromise.resolve(); + return Promise.resolve({}); + }); + + secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); + }); + + it("should set the default key id", async function () { + const setDefaultPromise = secretStorage.setDefaultKeyId("keyId"); + await accountDataPromise.promise; + + expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", { + key: "keyId", + }); + + accountDataAdapter.emit( + ClientEvent.AccountData, + new MatrixEvent({ + type: "m.secret_storage.default_key", + content: { key: "keyId" }, + }), + ); + await setDefaultPromise; + }); + + it("should set the default key id with a null key id", async function () { + const setDefaultPromise = secretStorage.setDefaultKeyId(null); + await accountDataPromise.promise; + + expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", {}); + + accountDataAdapter.emit( + ClientEvent.AccountData, + new MatrixEvent({ + type: "m.secret_storage.default_key", + content: {}, + }), + ); + await setDefaultPromise; + }); + }); + + describe("getDefaultKeyId", function () { + it("should return null when there is no key", async function () { + const accountDataAdapter = mockAccountDataClient(); + const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); + expect(await secretStorage.getDefaultKeyId()).toBe(null); + }); + + it("should return the key id when there is a key", async function () { + const accountDataAdapter = mockAccountDataClient(); + accountDataAdapter.getAccountDataFromServer.mockResolvedValue({ key: "keyId" }); + const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); + expect(await secretStorage.getDefaultKeyId()).toBe("keyId"); + }); + + it("should return null when an empty object is in the account data", async function () { + const accountDataAdapter = mockAccountDataClient(); + accountDataAdapter.getAccountDataFromServer.mockResolvedValue({}); + const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {}); + expect(await secretStorage.getDefaultKeyId()).toBe(null); + }); + }); }); describe("trimTrailingEquals", () => { @@ -291,8 +365,13 @@ describe("trimTrailingEquals", () => { }); function mockAccountDataClient(): Mocked { + const eventEmitter = new TypedEventEmitter(); return { getAccountDataFromServer: jest.fn().mockResolvedValue(null), setAccountData: jest.fn().mockResolvedValue({}), + on: eventEmitter.on.bind(eventEmitter), + off: eventEmitter.off.bind(eventEmitter), + removeListener: eventEmitter.removeListener.bind(eventEmitter), + emit: eventEmitter.emit.bind(eventEmitter), } as unknown as Mocked; } diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 84831617775..96f892cf94b 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -264,7 +264,10 @@ class AccountDataClientAdapter return event?.getContent() ?? null; } - public setAccountData(type: K, content: AccountDataEvents[K]): Promise<{}> { + public setAccountData( + type: K, + content: AccountDataEvents[K] | Record, + ): Promise<{}> { const event = new MatrixEvent({ type, content }); const lastEvent = this.values.get(type); this.values.set(type, event); diff --git a/src/secret-storage.ts b/src/secret-storage.ts index 2d6cd8a2793..565e9ead295 100644 --- a/src/secret-storage.ts +++ b/src/secret-storage.ts @@ -148,7 +148,10 @@ export interface AccountDataClient extends TypedEventEmitter(eventType: K, content: AccountDataEvents[K]) => Promise<{}>; + setAccountData: ( + eventType: K, + content: AccountDataEvents[K] | Record, + ) => Promise<{}>; } /** @@ -316,9 +319,12 @@ export interface ServerSideSecretStorage { /** * Set the default key ID for encrypting secrets. * + * If keyId is `null`, the default key id value in the account data will be set to an empty object. + * This is considered as "disabling" the default key. + * * @param keyId - The new default key ID */ - setDefaultKeyId(keyId: string): Promise; + setDefaultKeyId(keyId: string | null): Promise; } /** @@ -357,21 +363,33 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage { } /** - * Set the default key ID for encrypting secrets. - * - * @param keyId - The new default key ID + * Implementation of {@link ServerSideSecretStorage#setDefaultKeyId}. */ - public setDefaultKeyId(keyId: string): Promise { + public setDefaultKeyId(keyId: string | null): Promise { return new Promise((resolve, reject) => { const listener = (ev: MatrixEvent): void => { - if (ev.getType() === "m.secret_storage.default_key" && ev.getContent().key === keyId) { + if (ev.getType() !== "m.secret_storage.default_key") { + // Different account data item + return; + } + + // If keyId === null, the content should be an empty object. + // Otherwise, the `key` in the content object should match keyId. + const content = ev.getContent(); + const isSameKey = keyId === null ? Object.keys(content).length === 0 : content.key === keyId; + if (isSameKey) { this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener); resolve(); } }; this.accountDataAdapter.on(ClientEvent.AccountData, listener); - this.accountDataAdapter.setAccountData("m.secret_storage.default_key", { key: keyId }).catch((e) => { + // The spec [1] says that the value of the account data entry should be an object with a `key` property. + // It doesn't specify how to delete the default key; we do it by setting the account data to an empty object. + // + // [1]: https://spec.matrix.org/v1.13/client-server-api/#key-storage + const newValue: Record | { key: string } = keyId === null ? {} : { key: keyId }; + this.accountDataAdapter.setAccountData("m.secret_storage.default_key", newValue).catch((e) => { this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener); reject(e); }); From bdd4d82cb3587d6d6285d05bb31d07796ca3959e Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 15 Jan 2025 13:14:05 -0500 Subject: [PATCH 55/55] Distinguish room state and timeline events in embedded clients (#4574) * Distinguish room state and timeline events in embedded clients This change enables room widget clients to take advantage of the more reliable method of communicating room state over the widget API provided by a recent update to MSC2762. * Add missing awaits * Upgrade matrix-widget-api --- spec/unit/embedded.spec.ts | 56 +++++++++++++++++------- src/embedded.ts | 89 ++++++++++++++++++-------------------- yarn.lock | 6 +-- 3 files changed, 84 insertions(+), 67 deletions(-) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index c5ef3a6a2c6..5e35c02c969 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -28,7 +28,6 @@ import { WidgetApiToWidgetAction, MatrixCapabilities, ITurnServer, - IRoomEvent, IOpenIDCredentials, ISendEventFromWidgetResponseData, WidgetApiResponseError, @@ -635,12 +634,20 @@ describe("RoomWidgetClient", () => { }); it("receives", async () => { - await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar"); + // Client needs to be told that the room state is loaded + widgetApi.emit( + `action:${WidgetApiToWidgetAction.UpdateState}`, + new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }), + ); + await init; const emittedEvent = new Promise((resolve) => client.once(ClientEvent.Event, resolve)); const emittedSync = new Promise((resolve) => client.once(ClientEvent.Sync, resolve)); + // Let's assume that a state event comes in but it doesn't actually + // update the state of the room just yet (maybe it's unauthorized) widgetApi.emit( `action:${WidgetApiToWidgetAction.SendEvent}`, new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }), @@ -649,26 +656,43 @@ describe("RoomWidgetClient", () => { // The client should've emitted about the received event expect((await emittedEvent).getEffectiveEvent()).toEqual(event); expect(await emittedSync).toEqual(SyncState.Syncing); - // It should've also inserted the event into the room object + // However it should not have changed the room state const room = client.getRoom("!1:example.org"); - expect(room).not.toBeNull(); + expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toBe(null); + + // Now assume that the state event becomes favored by state + // resolution for whatever reason and enters into the current state + // of the room + widgetApi.emit( + `action:${WidgetApiToWidgetAction.UpdateState}`, + new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { + detail: { data: { state: [event] } }, + }), + ); + // It should now have changed the room state expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event); }); - it("backfills", async () => { - widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) => - eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar" - ? [event as IRoomEvent] - : [], + it("ignores state updates for other rooms", async () => { + const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + // Client needs to be told that the room state is loaded + widgetApi.emit( + `action:${WidgetApiToWidgetAction.UpdateState}`, + new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }), ); + await init; - await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); - expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); - expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar"); - - const room = client.getRoom("!1:example.org"); - expect(room).not.toBeNull(); - expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event); + // Now a room we're not interested in receives a state update + widgetApi.emit( + `action:${WidgetApiToWidgetAction.UpdateState}`, + new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { + detail: { data: { state: [{ ...event, room_id: "!other-room:example.org" }] } }, + }), + ); + // No change to the room state + for (const room of client.getRooms()) { + expect(room.currentState.getStateEvents("org.example.foo", "bar")).toBe(null); + } }); }); diff --git a/src/embedded.ts b/src/embedded.ts index b0cc4c158e8..53154e40e62 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -28,6 +28,7 @@ import { WidgetApiAction, IWidgetApiResponse, IWidgetApiResponseData, + IUpdateStateToWidgetActionRequest, } from "matrix-widget-api"; import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event.ts"; @@ -136,6 +137,7 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: () export class RoomWidgetClient extends MatrixClient { private room?: Room; private readonly widgetApiReady: Promise; + private readonly roomStateSynced: Promise; private lifecycle?: AbortController; private syncState: SyncState | null = null; @@ -189,6 +191,11 @@ export class RoomWidgetClient extends MatrixClient { }; this.widgetApiReady = new Promise((resolve) => this.widgetApi.once("ready", resolve)); + this.roomStateSynced = capabilities.receiveState?.length + ? new Promise((resolve) => + this.widgetApi.once(`action:${WidgetApiToWidgetAction.UpdateState}`, resolve), + ) + : Promise.resolve(); // Request capabilities for the functionality this client needs to support if ( @@ -241,6 +248,7 @@ export class RoomWidgetClient extends MatrixClient { widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate); // Open communication with the host widgetApi.start(); @@ -276,28 +284,6 @@ export class RoomWidgetClient extends MatrixClient { await this.widgetApiReady; - // Backfill the requested events - // We only get the most recent event for every type + state key combo, - // so it doesn't really matter what order we inject them in - await Promise.all( - this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => { - const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]); - const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial)); - - if (this.syncApi instanceof SyncApi) { - // Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode - // -> state events in `timelineEventList` will update the state. - await this.syncApi.injectRoomEvents(this.room!, undefined, events); - } else { - await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync - } - events.forEach((event) => { - this.emit(ClientEvent.Event, event); - logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); - }); - }) ?? [], - ); - if (opts.clientWellKnownPollPeriod !== undefined) { this.clientWellKnownIntervalID = setInterval(() => { this.fetchClientWellKnown(); @@ -305,8 +291,9 @@ export class RoomWidgetClient extends MatrixClient { this.fetchClientWellKnown(); } + await this.roomStateSynced; this.setSyncState(SyncState.Syncing); - logger.info("Finished backfilling events"); + logger.info("Finished initial sync"); this.matrixRTC.start(); @@ -317,6 +304,7 @@ export class RoomWidgetClient extends MatrixClient { public stopClient(): void { this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate); super.stopClient(); this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped @@ -574,36 +562,15 @@ export class RoomWidgetClient extends MatrixClient { // Only inject once we have update the txId await this.updateTxId(event); - // The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now. if (this.syncApi instanceof SyncApi) { - // The code will want to be something like: - // ``` - // if (!params.addToTimeline && !params.addToState) { - // // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode" - // // -> state events part of the `timelineEventList` parameter will update the state. - // this.injectRoomEvents(this.room!, [], undefined, [event]); - // } else { - // this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []); - // } - // ``` - - // Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode - // -> state events in `timelineEventList` will update the state. - await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]); + await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]); } else { - // The code will want to be something like: - // ``` - // if (!params.addToTimeline && !params.addToState) { - // this.injectRoomEvents(this.room!, [], [event]); - // } else { - // this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []); - // } - // ``` - await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync + // Sliding Sync + await this.syncApi!.injectRoomEvents(this.room!, [], [event]); } this.emit(ClientEvent.Event, event); this.setSyncState(SyncState.Syncing); - logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + logger.info(`Received event ${event.getId()} ${event.getType()}`); } else { const { event_id: eventId, room_id: roomId } = ev.detail.data; logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`); @@ -628,6 +595,32 @@ export class RoomWidgetClient extends MatrixClient { await this.ack(ev); }; + private onStateUpdate = async (ev: CustomEvent): Promise => { + ev.preventDefault(); + + for (const rawEvent of ev.detail.data.state) { + // Verify the room ID matches, since it's possible for the client to + // send us state updates from other rooms if this widget is always + // on screen + if (rawEvent.room_id === this.roomId) { + const event = new MatrixEvent(rawEvent as Partial); + + if (this.syncApi instanceof SyncApi) { + await this.syncApi.injectRoomEvents(this.room!, undefined, [event]); + } else { + // Sliding Sync + await this.syncApi!.injectRoomEvents(this.room!, [event]); + } + logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`); + } else { + const { event_id: eventId, room_id: roomId } = ev.detail.data; + logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`); + } + } + + await this.ack(ev); + }; + private async watchTurnServers(): Promise { const servers = this.widgetApi.getTurnServers(); const onClientStopped = (): void => { diff --git a/yarn.lock b/yarn.lock index 62eeb795ba7..06e4d7ce0d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4854,9 +4854,9 @@ matrix-mock-request@^2.5.0: expect "^28.1.0" matrix-widget-api@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55" - integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw== + version "1.12.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz#b3d22bab1670051c8eeee66bb96d08b33148bc99" + integrity sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww== dependencies: "@types/events" "^3.0.0" events "^3.2.0"