1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-22 05:22:39 +03:00

Merge remote-tracking branch 'origin/develop' into hs/use-rtc-slots-for-call

This commit is contained in:
Half-Shot
2025-11-03 13:48:30 +00:00
14 changed files with 464 additions and 891 deletions

View File

@@ -15,7 +15,7 @@ jobs:
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -4,8 +4,6 @@ on:
secrets:
ELEMENT_BOT_TOKEN:
required: true
NPM_TOKEN:
required: false
GPG_PASSPHRASE:
required: false
GPG_PRIVATE_KEY:
@@ -282,8 +280,6 @@ jobs:
permissions:
contents: read
id-token: write
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
post-release:
name: Post release steps

View File

@@ -1,9 +1,6 @@
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
outputs:
id:
description: "The npm package@version string we published"
@@ -31,21 +28,15 @@ jobs:
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
run: |
npm publish --provenance --access public --tag next
release=$(jq -r '"\(.name)@\(.version)"' package.json)
echo "id=$release" >> $GITHUB_OUTPUT
run: npm publish --provenance --access public --tag "$TAG"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: 🎖️ Add `latest` dist-tag to final releases
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
run: npm dist-tag add "$release" latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.id }}
TAG: contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest'

View File

@@ -43,7 +43,7 @@ jobs:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: 📥 Download artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,7 +51,7 @@ jobs:
name: coverage
path: coverage
- name: 📥 Download sharded artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
if: inputs.sharded
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -114,7 +114,7 @@ jobs:
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: docs
path: _docs

View File

@@ -57,7 +57,7 @@ jobs:
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
path: |

View File

@@ -59,7 +59,7 @@
"matrix-widget-api": "^1.10.0",
"oidc-client-ts": "^3.0.1",
"p-retry": "7",
"sdp-transform": "^2.14.1",
"sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6",
"uuid": "13"
},

View File

@@ -22,9 +22,7 @@ import { type ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTra
import { KeyTransportEvents, type KeyTransportEventsHandlerMap } from "../../../src/matrixrtc/IKeyTransport.ts";
import { sessionMembershipTemplate, mockCallMembership } from "./mocks.ts";
import { decodeBase64, TypedEventEmitter } from "../../../src";
import { RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
import { type RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
import { logger, type Logger } from "../../../src/logger.ts";
import { logger } from "../../../src/logger.ts";
import { getParticipantId } from "../../../src/matrixrtc/utils.ts";
describe("RTCEncryptionManager", () => {
@@ -782,86 +780,6 @@ describe("RTCEncryptionManager", () => {
);
});
it("Should re-distribute key on transport switch", async () => {
const toDeviceEmitter = new TypedEventEmitter<KeyTransportEvents, KeyTransportEventsHandlerMap>();
const mockToDeviceTransport = {
start: jest.fn(),
stop: jest.fn(),
sendKey: jest.fn().mockResolvedValue(undefined),
on: toDeviceEmitter.on.bind(toDeviceEmitter),
off: toDeviceEmitter.off.bind(toDeviceEmitter),
emit: toDeviceEmitter.emit.bind(toDeviceEmitter),
setParentLogger: jest.fn(),
} as unknown as Mocked<ToDeviceKeyTransport>;
const roomEmitter = new TypedEventEmitter<KeyTransportEvents, KeyTransportEventsHandlerMap>();
const mockRoomTransport = {
start: jest.fn(),
stop: jest.fn(),
sendKey: jest.fn().mockResolvedValue(undefined),
on: roomEmitter.on.bind(roomEmitter),
off: roomEmitter.off.bind(roomEmitter),
emit: roomEmitter.emit.bind(roomEmitter),
setParentLogger: jest.fn(),
} as unknown as Mocked<RoomKeyTransport>;
const mockLogger = {
debug: jest.fn(),
warn: jest.fn(),
} as unknown as Mocked<Logger>;
const transport = new RoomAndToDeviceTransport(mockToDeviceTransport, mockRoomTransport, {
getChild: jest.fn().mockReturnValue(mockLogger),
} as unknown as Mocked<Logger>);
encryptionManager = new RTCEncryptionManager(
"@alice:example.org",
"DEVICE01",
getMembershipMock,
transport,
statistics,
onEncryptionKeysChanged,
);
const members = [
aCallMembership("@bob:example.org", "BOBDEVICE"),
aCallMembership("@bob:example.org", "BOBDEVICE2"),
aCallMembership("@carl:example.org", "CARLDEVICE"),
];
getMembershipMock.mockReturnValue(members);
// Let's join
encryptionManager.join(undefined);
encryptionManager.onMembershipsUpdate();
await jest.advanceTimersByTimeAsync(10);
// Should have sent the key to the toDevice transport
expect(mockToDeviceTransport.sendKey).toHaveBeenCalledTimes(1);
expect(mockRoomTransport.sendKey).not.toHaveBeenCalled();
// Simulate receiving a key by room transport
roomEmitter.emit(
KeyTransportEvents.ReceivedKeys,
"@bob:example.org",
"BOBDEVICE",
"AAAAAAAAAAA",
0 /* KeyId */,
0 /* Timestamp */,
);
await jest.runOnlyPendingTimersAsync();
// The key should have been re-distributed to the room transport
expect(mockRoomTransport.sendKey).toHaveBeenCalled();
expect(mockToDeviceTransport.sendKey).toHaveBeenCalledWith(
expect.any(String),
// It is the first key re-distributed
0,
// to all the members
members.map((m) => ({ userId: m.sender, deviceId: m.deviceId, membershipTs: m.createdTs() })),
);
});
function aCallMembership(userId: string, deviceId: string, ts: number = 1000): CallMembership {
return mockCallMembership(
{ ...sessionMembershipTemplate, user_id: userId, device_id: deviceId, created_ts: ts },

View File

@@ -1,187 +0,0 @@
/*
Copyright 2025 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 { type Mocked } from "jest-mock";
import { makeKey, makeMockEvent, makeMockRoom } from "./mocks";
import { EventType, type IRoomTimelineData, type Room, RoomEvent, type MatrixClient } from "../../../src";
import { ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTransport.ts";
import {
getMockClientWithEventEmitter,
mockClientMethodsEvents,
mockClientMethodsUser,
} from "../../test-utils/client.ts";
import { type ParticipantDeviceInfo, type Statistics } from "../../../src/matrixrtc";
import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport.ts";
import { type Logger } from "../../../src/logger.ts";
import { RoomAndToDeviceEvents, RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
import { RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
describe("RoomAndToDeviceTransport", () => {
const roomId = "!room:id";
let mockClient: Mocked<MatrixClient>;
let statistics: Statistics;
let mockLogger: Mocked<Logger>;
let transport: RoomAndToDeviceTransport;
let mockRoom: Room;
let sendEventMock: jest.Mock;
let roomKeyTransport: RoomKeyTransport;
let toDeviceKeyTransport: ToDeviceKeyTransport;
let toDeviceSendKeySpy: jest.SpyInstance;
let roomSendKeySpy: jest.SpyInstance;
beforeEach(() => {
sendEventMock = jest.fn();
mockClient = getMockClientWithEventEmitter({
encryptAndSendToDevice: jest.fn(),
getDeviceId: jest.fn().mockReturnValue("MYDEVICE"),
...mockClientMethodsEvents(),
...mockClientMethodsUser("@alice:example.org"),
sendEvent: sendEventMock,
});
mockRoom = makeMockRoom([]);
mockLogger = {
debug: jest.fn(),
warn: jest.fn(),
getChild: jest.fn(),
} as unknown as Mocked<Logger>;
mockLogger.getChild.mockReturnValue(mockLogger);
statistics = {
counters: {
roomEventEncryptionKeysSent: 0,
roomEventEncryptionKeysReceived: 0,
},
totals: {
roomEventEncryptionKeysReceivedTotalAge: 0,
},
};
roomKeyTransport = new RoomKeyTransport(mockRoom, mockClient, statistics);
toDeviceKeyTransport = new ToDeviceKeyTransport(
"@alice:example.org",
"MYDEVICE",
mockRoom.roomId,
mockClient,
statistics,
);
transport = new RoomAndToDeviceTransport(toDeviceKeyTransport, roomKeyTransport, mockLogger);
toDeviceSendKeySpy = jest.spyOn(toDeviceKeyTransport, "sendKey");
roomSendKeySpy = jest.spyOn(roomKeyTransport, "sendKey");
});
it("should enable to device transport when starting", () => {
transport.start();
expect(transport.enabled.room).toBeFalsy();
expect(transport.enabled.toDevice).toBeTruthy();
});
it("only sends to device keys when sending a key", async () => {
transport.start();
await transport.sendKey("1235", 0, [
{ userId: "@alice:example.org", deviceId: "ALICEDEVICE", membershipTs: 1234 },
]);
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(1);
expect(roomSendKeySpy).toHaveBeenCalledTimes(0);
expect(transport.enabled.room).toBeFalsy();
expect(transport.enabled.toDevice).toBeTruthy();
});
it("enables room transport and disables to device transport when receiving a room key", async () => {
transport.start();
const onNewKeyFromTransport = jest.fn();
const onTransportEnabled = jest.fn();
transport.on(KeyTransportEvents.ReceivedKeys, onNewKeyFromTransport);
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
mockRoom.emit(
RoomEvent.Timeline,
makeMockEvent(EventType.CallEncryptionKeysPrefix, "@bob:example.org", roomId, {
call_id: "",
keys: [makeKey(0, "testKey")],
sent_ts: Date.now(),
device_id: "AAAAAAA",
}),
undefined,
undefined,
false,
{} as IRoomTimelineData,
);
await jest.advanceTimersByTimeAsync(1);
expect(transport.enabled.room).toBeTruthy();
expect(transport.enabled.toDevice).toBeFalsy();
await transport.sendKey("1235", 0, [
{ userId: "@alice:example.org", deviceId: "AlICEDEV", membershipTs: 1234 },
]);
expect(sendEventMock).toHaveBeenCalledTimes(1);
expect(roomSendKeySpy).toHaveBeenCalledTimes(1);
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(0);
expect(onTransportEnabled).toHaveBeenCalledWith({ toDevice: false, room: true });
});
it("enables room transport and disables to device transport on widget driver error", async () => {
mockClient.encryptAndSendToDevice.mockRejectedValue({
message:
"unknown variant `send_to_device`, expected one of `supported_api_versions`, `content_loaded`, `get_openid`, `org.matrix.msc2876.read_events`, `send_event`, `org.matrix.msc4157.update_delayed_event` at line 1 column 22",
});
transport.start();
const membership: ParticipantDeviceInfo = {
userId: "@alice:example.org",
deviceId: "ALICEDEVICE",
membershipTs: 1234,
};
const onTransportEnabled = jest.fn();
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
// We start with toDevice transport enabled
expect(transport.enabled.room).toBeFalsy();
expect(transport.enabled.toDevice).toBeTruthy();
await transport.sendKey("1235", 0, [membership]);
// We switched transport, now room transport is enabled
expect(onTransportEnabled).toHaveBeenCalledWith({ toDevice: false, room: true });
expect(transport.enabled.room).toBeTruthy();
expect(transport.enabled.toDevice).toBeFalsy();
// sanity check that we called the failang to device send key.
expect(toDeviceKeyTransport.sendKey).toHaveBeenCalledWith("1235", 0, [membership]);
expect(toDeviceKeyTransport.sendKey).toHaveBeenCalledTimes(1);
// We re-sent the key via the room transport
expect(roomKeyTransport.sendKey).toHaveBeenCalledWith("1235", 0, [membership]);
expect(roomKeyTransport.sendKey).toHaveBeenCalledTimes(1);
mockClient.encryptAndSendToDevice.mockRestore();
});
it("does log that it did nothing when disabled", () => {
transport.start();
const onNewKeyFromTransport = jest.fn();
const onTransportEnabled = jest.fn();
transport.on(KeyTransportEvents.ReceivedKeys, onNewKeyFromTransport);
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
transport.setEnabled({ toDevice: false, room: false });
const dateNow = Date.now();
roomKeyTransport.emit(KeyTransportEvents.ReceivedKeys, "user", "device", "roomKey", 0, dateNow);
toDeviceKeyTransport.emit(KeyTransportEvents.ReceivedKeys, "user", "device", "toDeviceKey", 0, Date.now());
expect(mockLogger.debug).toHaveBeenCalledWith("To Device transport is disabled, ignoring received keys");
// for room key transport we will never get a disabled message because its will always just turn on
expect(onTransportEnabled).toHaveBeenNthCalledWith(1, { toDevice: false, room: false });
expect(onTransportEnabled).toHaveBeenNthCalledWith(2, { toDevice: false, room: true });
expect(onNewKeyFromTransport).toHaveBeenCalledTimes(1);
expect(onNewKeyFromTransport).toHaveBeenCalledWith("user", "device", "roomKey", 0, dateNow);
});
});

View File

@@ -7,11 +7,6 @@ import { type CallMembership } from "./CallMembership.ts";
import { type KeyTransportEventListener, KeyTransportEvents, type IKeyTransport } from "./IKeyTransport.ts";
import { isMyMembership, type ParticipantId, type Statistics } from "./types.ts";
import { getParticipantId } from "./utils.ts";
import {
type EnabledTransports,
RoomAndToDeviceEvents,
RoomAndToDeviceTransport,
} from "./RoomAndToDeviceKeyTransport.ts";
/**
* This interface is for testing and for making it possible to interchange the encryption manager.
@@ -119,10 +114,7 @@ export class EncryptionManager implements IEncryptionManager {
this.manageMediaKeys = this.joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
// Deprecate RoomKeyTransport: this can get removed.
if (this.transport instanceof RoomAndToDeviceTransport) {
this.transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, this.onTransportChanged);
}
this.transport.start();
if (this.joinConfig?.manageMediaKeys) {
this.makeNewSenderKey();
@@ -315,10 +307,6 @@ export class EncryptionManager implements IEncryptionManager {
}
};
private onTransportChanged: (enabled: EnabledTransports) => void = () => {
this.requestSendCurrentKey();
};
public onNewKeyReceived: KeyTransportEventListener = (userId, deviceId, keyBase64Encoded, index, timestamp) => {
this.logger.debug(`Received key over key transport ${userId}:${deviceId} at index ${index}`);
this.setEncryptionKey(userId, deviceId, index, keyBase64Encoded, timestamp);

View File

@@ -38,25 +38,20 @@ import {
type RtcSlotEventContent,
type SlotDescription,
} from "./types.ts";
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
import {
MembershipManagerEvent,
type MembershipManagerEventHandlerMap,
type IMembershipManager,
} from "./IMembershipManager.ts";
import { RTCEncryptionManager } from "./RTCEncryptionManager.ts";
import {
RoomAndToDeviceEvents,
type RoomAndToDeviceEventsHandlerMap,
RoomAndToDeviceTransport,
} from "./RoomAndToDeviceKeyTransport.ts";
import { TypedReEmitter } from "../ReEmitter.ts";
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
import { TypedReEmitter } from "../ReEmitter.ts";
import { type MatrixEvent } from "../models/event.ts";
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../models/room-sticky-events.ts";
import { DefaultCallApplicationSlot } from "./CallApplication.ts";
import { slotDescriptionToId } from "./utils.ts";
import { RtcMembershipData } from "./membership/rtc.ts";
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
/**
* Events emitted by MatrixRTCSession
@@ -247,8 +242,8 @@ interface SessionMembershipsForRoomOpts {
* This class doesn't deal with media at all, just membership & properties of a session.
*/
export class MatrixRTCSession extends TypedEventEmitter<
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
MatrixRTCSessionEvent | MembershipManagerEvent,
MatrixRTCSessionEventHandlerMap & MembershipManagerEventHandlerMap
> {
private membershipManager?: IMembershipManager;
private encryptionManager?: IEncryptionManager;
@@ -582,8 +577,8 @@ export class MatrixRTCSession extends TypedEventEmitter<
}
private reEmitter = new TypedReEmitter<
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
MatrixRTCSessionEvent | MembershipManagerEvent,
MatrixRTCSessionEventHandlerMap & MembershipManagerEventHandlerMap
>(this);
/**
@@ -631,13 +626,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
this.logger.info("Using to-device with room fallback transport for encryption keys");
const [uId, dId] = [this.client.getUserId()!, this.client.getDeviceId()!];
const [room, client, statistics] = [this.roomSubset, this.client, this.statistics];
// Deprecate RoomKeyTransport: only ToDeviceKeyTransport is needed once deprecated
const roomKeyTransport = new RoomKeyTransport(room, client, statistics);
const toDeviceTransport = new ToDeviceKeyTransport(uId, dId, room.roomId, client, statistics);
transport = new RoomAndToDeviceTransport(toDeviceTransport, roomKeyTransport, this.logger);
// Expose the changes so the ui can display the currently used transport.
this.reEmitter.reEmit(transport, [RoomAndToDeviceEvents.EnabledTransportsChanged]);
const transport = new ToDeviceKeyTransport(uId, dId, room.roomId, client, statistics);
this.encryptionManager = new RTCEncryptionManager(
this.client.getUserId()!,
this.client.getDeviceId()!,

View File

@@ -29,11 +29,6 @@ import type {
Statistics,
} from "./types.ts";
import { getParticipantId, OutdatedKeyFilter } from "./utils.ts";
import {
type EnabledTransports,
RoomAndToDeviceEvents,
RoomAndToDeviceTransport,
} from "./RoomAndToDeviceKeyTransport.ts";
/**
* RTCEncryptionManager is used to manage the encryption keys for a call.
@@ -137,10 +132,6 @@ export class RTCEncryptionManager implements IEncryptionManager {
this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;
this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10_000;
this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
// Deprecate RoomKeyTransport: this can get removed.
if (this.transport instanceof RoomAndToDeviceTransport) {
this.transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, this.onTransportChanged);
}
this.transport.start();
}
@@ -151,29 +142,6 @@ export class RTCEncryptionManager implements IEncryptionManager {
this.participantKeyRings.clear();
}
// Temporary for backwards compatibility
// TODO: Remove this in the future
private onTransportChanged: (enabled: EnabledTransports) => void = () => {
this.logger?.info("Transport change detected, restarting key distribution");
if (this.currentKeyDistributionPromise) {
this.currentKeyDistributionPromise
.then(() => {
if (this.outboundSession) {
this.outboundSession.sharedWith = [];
this.ensureKeyDistribution();
}
})
.catch((e) => {
this.logger?.error("Failed to restart key distribution", e);
});
} else {
if (this.outboundSession) {
this.outboundSession.sharedWith = [];
this.ensureKeyDistribution();
}
}
};
/**
* Will ensure that a new key is distributed and used to encrypt our media.
* If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed.

View File

@@ -1,131 +0,0 @@
/*
Copyright 2025 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 { logger as rootLogger, type Logger } from "../logger.ts";
import { KeyTransportEvents, type KeyTransportEventsHandlerMap, type IKeyTransport } from "./IKeyTransport.ts";
import type { RoomKeyTransport } from "./RoomKeyTransport.ts";
import { NotSupportedError, type ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { type ParticipantDeviceInfo } from "./types.ts";
// Deprecate RoomAndToDeviceTransport: This whole class is only a stop gap until we remove RoomKeyTransport.
export interface EnabledTransports {
toDevice: boolean;
room: boolean;
}
export enum RoomAndToDeviceEvents {
EnabledTransportsChanged = "enabled_transports_changed",
}
export type RoomAndToDeviceEventsHandlerMap = {
[RoomAndToDeviceEvents.EnabledTransportsChanged]: (enabledTransports: EnabledTransports) => void;
};
/**
* A custom transport that subscribes to room key events (via `RoomKeyTransport`) and to device key events (via: `ToDeviceKeyTransport`)
* The public setEnabled method allows to turn one or the other on or off on the fly.
* It will emit `RoomAndToDeviceEvents.EnabledTransportsChanged` if the enabled transport changes to allow comminitcating this to
* the user in the ui.
*
* Since it will always subscribe to both (room and to device) but only emit for the enabled ones, it can detect
* if a room key event was received and autoenable it.
*/
export class RoomAndToDeviceTransport
extends TypedEventEmitter<
KeyTransportEvents | RoomAndToDeviceEvents,
KeyTransportEventsHandlerMap & RoomAndToDeviceEventsHandlerMap
>
implements IKeyTransport
{
private readonly logger: Logger;
private _enabled: EnabledTransports = { toDevice: true, room: false };
public constructor(
private toDeviceTransport: ToDeviceKeyTransport,
private roomKeyTransport: RoomKeyTransport,
parentLogger?: Logger,
) {
super();
this.logger = (parentLogger ?? rootLogger).getChild(`[RoomAndToDeviceTransport]`);
// update parent loggers for the sub transports so filtering for `RoomAndToDeviceTransport` contains their logs too
this.toDeviceTransport.setParentLogger(this.logger);
this.roomKeyTransport.setParentLogger(this.logger);
this.roomKeyTransport.on(KeyTransportEvents.ReceivedKeys, (...props) => {
// Turn on the room transport if we receive a roomKey from another participant
// and disable the toDevice transport.
if (!this._enabled.room) {
this.logger.debug("Received room key, enabling room key transport, disabling toDevice transport");
this.setEnabled({ toDevice: false, room: true });
}
this.emit(KeyTransportEvents.ReceivedKeys, ...props);
});
this.toDeviceTransport.on(KeyTransportEvents.ReceivedKeys, (...props) => {
if (this._enabled.toDevice) {
this.emit(KeyTransportEvents.ReceivedKeys, ...props);
} else {
this.logger.debug("To Device transport is disabled, ignoring received keys");
}
});
}
/** Set which transport type should be used to send and receive keys.*/
public setEnabled(enabled: { toDevice: boolean; room: boolean }): void {
if (this.enabled.toDevice !== enabled.toDevice || this.enabled.room !== enabled.room) {
this._enabled = enabled;
this.emit(RoomAndToDeviceEvents.EnabledTransportsChanged, enabled);
}
}
/** The currently enabled transports that are used to send and receive keys.*/
public get enabled(): EnabledTransports {
return this._enabled;
}
public start(): void {
// always start the underlying transport since we need to enable room transport
// when someone else sends us a room key. (we need to listen to roomKeyTransport)
this.roomKeyTransport.start();
this.toDeviceTransport.start();
}
public stop(): void {
// always stop since it is always running
this.roomKeyTransport.stop();
this.toDeviceTransport.stop();
}
public async sendKey(keyBase64Encoded: string, index: number, members: ParticipantDeviceInfo[]): Promise<void> {
this.logger.debug(
`Sending key with index ${index} to call members (count=${members.length}) via:` +
(this._enabled.room ? "room transport" : "") +
(this._enabled.room && this._enabled.toDevice ? "and" : "") +
(this._enabled.toDevice ? "to device transport" : ""),
);
if (this._enabled.room) await this.roomKeyTransport.sendKey(keyBase64Encoded, index, members);
if (this._enabled.toDevice) {
try {
await this.toDeviceTransport.sendKey(keyBase64Encoded, index, members);
} catch (error) {
if (error instanceof NotSupportedError && !this._enabled.room) {
this.logger.warn(
"To device is not supported enabling room key transport, disabling toDevice transport",
);
this.setEnabled({ toDevice: false, room: true });
await this.sendKey(keyBase64Encoded, index, members);
}
}
}
}
}

845
yarn.lock

File diff suppressed because it is too large Load Diff