matrix-js-sdk 43.0.0 → 44.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/matrixrtc/CallMembership.d.ts +30 -25
  3. package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
  4. package/lib/matrixrtc/CallMembership.js +91 -100
  5. package/lib/matrixrtc/CallMembership.js.map +1 -1
  6. package/lib/matrixrtc/EncryptionManager.d.ts +6 -4
  7. package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
  8. package/lib/matrixrtc/EncryptionManager.js.map +1 -1
  9. package/lib/matrixrtc/IMembershipManager.d.ts +2 -15
  10. package/lib/matrixrtc/IMembershipManager.d.ts.map +1 -1
  11. package/lib/matrixrtc/IMembershipManager.js.map +1 -1
  12. package/lib/matrixrtc/LivekitTransport.d.ts +36 -7
  13. package/lib/matrixrtc/LivekitTransport.d.ts.map +1 -1
  14. package/lib/matrixrtc/LivekitTransport.js +29 -2
  15. package/lib/matrixrtc/LivekitTransport.js.map +1 -1
  16. package/lib/matrixrtc/MatrixRTCSession.d.ts +12 -51
  17. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
  18. package/lib/matrixrtc/MatrixRTCSession.js +10 -66
  19. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  20. package/lib/matrixrtc/MembershipManager.d.ts +5 -10
  21. package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
  22. package/lib/matrixrtc/MembershipManager.js +25 -44
  23. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  24. package/lib/matrixrtc/RTCEncryptionManager.d.ts +8 -8
  25. package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
  26. package/lib/matrixrtc/RTCEncryptionManager.js +30 -22
  27. package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
  28. package/lib/matrixrtc/ToDeviceKeyTransport.js +7 -8
  29. package/lib/matrixrtc/ToDeviceKeyTransport.js.map +1 -1
  30. package/lib/matrixrtc/membershipData/rtc.d.ts +8 -13
  31. package/lib/matrixrtc/membershipData/rtc.d.ts.map +1 -1
  32. package/lib/matrixrtc/membershipData/rtc.js +2 -11
  33. package/lib/matrixrtc/membershipData/rtc.js.map +1 -1
  34. package/lib/matrixrtc/membershipData/session.d.ts +6 -2
  35. package/lib/matrixrtc/membershipData/session.d.ts.map +1 -1
  36. package/lib/matrixrtc/membershipData/session.js +3 -0
  37. package/lib/matrixrtc/membershipData/session.js.map +1 -1
  38. package/lib/matrixrtc/types.d.ts +5 -0
  39. package/lib/matrixrtc/types.d.ts.map +1 -1
  40. package/lib/matrixrtc/types.js +5 -0
  41. package/lib/matrixrtc/types.js.map +1 -1
  42. package/lib/models/event-timeline-set.d.ts +9 -2
  43. package/lib/models/event-timeline-set.d.ts.map +1 -1
  44. package/lib/models/event-timeline-set.js +8 -5
  45. package/lib/models/event-timeline-set.js.map +1 -1
  46. package/lib/models/room-sticky-events.d.ts +17 -2
  47. package/lib/models/room-sticky-events.d.ts.map +1 -1
  48. package/lib/models/room-sticky-events.js +88 -47
  49. package/lib/models/room-sticky-events.js.map +1 -1
  50. package/lib/models/room.d.ts +1 -1
  51. package/lib/models/room.d.ts.map +1 -1
  52. package/lib/models/room.js +8 -2
  53. package/lib/models/room.js.map +1 -1
  54. package/lib/models/thread.d.ts.map +1 -1
  55. package/lib/models/thread.js +23 -2
  56. package/lib/models/thread.js.map +1 -1
  57. package/lib/rendezvous/MSC4108SignInWithQR.d.ts +3 -3
  58. package/lib/rendezvous/MSC4108SignInWithQR.d.ts.map +1 -1
  59. package/lib/rendezvous/MSC4108SignInWithQR.js.map +1 -1
  60. package/lib/rendezvous/SignInWithQRFlow.d.ts +95 -0
  61. package/lib/rendezvous/SignInWithQRFlow.d.ts.map +1 -0
  62. package/lib/rendezvous/SignInWithQRFlow.js +3 -0
  63. package/lib/rendezvous/SignInWithQRFlow.js.map +1 -0
  64. package/lib/rendezvous/index.d.ts +1 -0
  65. package/lib/rendezvous/index.d.ts.map +1 -1
  66. package/lib/rendezvous/index.js.map +1 -1
  67. package/lib/rust-crypto/index.js +5 -1
  68. package/lib/rust-crypto/index.js.map +1 -1
  69. package/package.json +3 -3
  70. package/src/matrixrtc/CallMembership.ts +85 -95
  71. package/src/matrixrtc/EncryptionManager.ts +6 -4
  72. package/src/matrixrtc/IMembershipManager.ts +2 -15
  73. package/src/matrixrtc/LivekitTransport.ts +58 -9
  74. package/src/matrixrtc/MatrixRTCSession.ts +21 -80
  75. package/src/matrixrtc/MembershipManager.ts +23 -40
  76. package/src/matrixrtc/RTCEncryptionManager.ts +51 -39
  77. package/src/matrixrtc/ToDeviceKeyTransport.ts +7 -7
  78. package/src/matrixrtc/membershipData/rtc.ts +9 -23
  79. package/src/matrixrtc/membershipData/session.ts +14 -2
  80. package/src/matrixrtc/types.ts +6 -0
  81. package/src/models/event-timeline-set.ts +19 -4
  82. package/src/models/room-sticky-events.ts +92 -58
  83. package/src/models/room.ts +8 -2
  84. package/src/models/thread.ts +25 -3
  85. package/src/rendezvous/MSC4108SignInWithQR.ts +3 -3
  86. package/src/rendezvous/SignInWithQRFlow.ts +117 -0
  87. package/src/rendezvous/index.ts +1 -0
  88. package/src/rust-crypto/index.ts +5 -1
@@ -21,6 +21,7 @@ import { KeyTransportEvents } from "./IKeyTransport.js";
21
21
  import { sleep } from "../utils.js";
22
22
  import { OutdatedKeyFilter } from "./utils.js";
23
23
  import { computeRtcIdentityRaw } from "./membershipData/rtc.js";
24
+ import { isUnstableLivekitTransport } from "./LivekitTransport.js";
24
25
 
25
26
  /**
26
27
  * Default for {@link EncryptionConfig.keyRotationParticipantLimit}.
@@ -61,8 +62,7 @@ export class RTCEncryptionManager {
61
62
  // to create a NoOpEncryptionManager that does nothing and use it for the session.
62
63
  // This will be done when removing the legacy EncryptionManager.
63
64
  _defineProperty(this, "manageMediaKeys", false);
64
- _defineProperty(this, "useHashedRtcBackendIdentity", false);
65
- _defineProperty(this, "ownRtcBackendIdentityCache", void 0);
65
+ _defineProperty(this, "publishedTransports", []);
66
66
  /**
67
67
  * Store the key rings for each participant.
68
68
  * The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.
@@ -113,6 +113,7 @@ export class RTCEncryptionManager {
113
113
  _defineProperty(this, "keyBuffer", new OutdatedKeyFilter());
114
114
  _defineProperty(this, "logger", undefined);
115
115
  _defineProperty(this, "rtcIdentityProvider", void 0);
116
+ _defineProperty(this, "ownBackendIdentitiesPromise", void 0);
116
117
  _defineProperty(this, "keysWithoutMatchingRTCMembership", []);
117
118
  _defineProperty(this, "onNewKeyReceived", (membership, keyBase64Encoded, index, timestamp) => {
118
119
  // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead
@@ -161,22 +162,25 @@ export class RTCEncryptionManager {
161
162
  if (this.keyRotationParticipantLimit === undefined) return false;
162
163
  return this.getMemberships().length >= this.keyRotationParticipantLimit;
163
164
  }
164
- async getOwnRtcBackendIdentity() {
165
- if (this.ownRtcBackendIdentityCache) return this.ownRtcBackendIdentityCache;
166
- if (this.useHashedRtcBackendIdentity) {
165
+ getOwnBackendIdentities() {
166
+ this.ownBackendIdentitiesPromise ??= (async () => {
167
167
  const {
168
168
  userId,
169
169
  deviceId,
170
170
  memberId
171
171
  } = this.ownMembership;
172
- this.logger?.info(
173
- // If we see this log multiple times, we need to reconsider the precompute call of getOwnRtcBackendIdentity
174
- `Computing RTC backend identity for ${userId}:${deviceId}:${memberId} (SHOULD ONLY BE CALLED ONCE)`);
175
- this.ownRtcBackendIdentityCache = await this.rtcIdentityProvider(userId, deviceId, memberId);
176
- } else {
177
- this.ownRtcBackendIdentityCache = `${this.ownMembership.userId}:${this.ownMembership.deviceId}`;
178
- }
179
- return this.ownRtcBackendIdentityCache;
172
+ const livekitTransports = this.publishedTransports.filter(isUnstableLivekitTransport);
173
+ const backendIdentities = [];
174
+ if (livekitTransports.some(t => "url" in t)) {
175
+ this.logger?.info(`Computing RTC backend identity for ${userId}:${deviceId}:${memberId}`);
176
+ backendIdentities.push(await this.rtcIdentityProvider(userId, memberId));
177
+ }
178
+ if (livekitTransports.some(t => "livekit_service_url" in t)) {
179
+ backendIdentities.push(`${userId}:${deviceId}`);
180
+ }
181
+ return backendIdentities;
182
+ })();
183
+ return this.ownBackendIdentitiesPromise;
180
184
  }
181
185
  getEncryptionKeys() {
182
186
  return new Map(this.participantKeyRings);
@@ -200,9 +204,11 @@ export class RTCEncryptionManager {
200
204
  });
201
205
  return;
202
206
  }
203
- this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, fullMembership.rtcBackendIdentity);
207
+ for (const backendIdentity of fullMembership.backendIdentities) {
208
+ this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, backendIdentity);
209
+ }
204
210
  }
205
- addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, rtcBackendIdentity) {
211
+ addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, backendIdentity) {
206
212
  const mapKey = getEncryptionKeyMapKey(membership);
207
213
  if (!this.participantKeyRings.has(mapKey)) {
208
214
  this.participantKeyRings.set(mapKey, []);
@@ -211,18 +217,19 @@ export class RTCEncryptionManager {
211
217
  key,
212
218
  keyIndex,
213
219
  membership,
214
- rtcBackendIdentity
220
+ backendIdentity
215
221
  });
216
- this.onEncryptionKeysChanged(key, keyIndex, membership, rtcBackendIdentity);
222
+ this.onEncryptionKeysChanged(key, keyIndex, membership, backendIdentity);
217
223
  }
218
- join(joinConfig) {
224
+ join(joinConfig, publishedTransports) {
219
225
  this.manageMediaKeys = joinConfig?.manageMediaKeys ?? true; // default to true
220
- this.useHashedRtcBackendIdentity = joinConfig?.unstableSendStickyEvents ?? false;
226
+ this.publishedTransports = publishedTransports;
227
+ this.ownBackendIdentitiesPromise = undefined;
221
228
  this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;
222
229
  this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10000;
223
230
  this.keyRotationParticipantLimit = joinConfig?.keyRotationParticipantLimit ?? DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;
224
231
  this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
225
- void this.getOwnRtcBackendIdentity(); // precompute own identity
232
+ void this.getOwnBackendIdentities(); // precompute own identity
226
233
 
227
234
  this.logger?.info(`Joining room`);
228
235
  this.transport.start();
@@ -289,7 +296,8 @@ export class RTCEncryptionManager {
289
296
  keyId: 0
290
297
  };
291
298
  this.outboundSession = firstKey;
292
- this.addKeyToParticipantWithBackendIdentity(firstKey.key, firstKey.keyId, this.ownMembership, await this.getOwnRtcBackendIdentity());
299
+ const ids = await this.getOwnBackendIdentities();
300
+ for (const backendIdentity of ids) this.addKeyToParticipantWithBackendIdentity(firstKey.key, firstKey.keyId, this.ownMembership, backendIdentity);
293
301
  }
294
302
  // get current memberships
295
303
  const toShareWith = this.getMemberships().filter(membership => {
@@ -357,7 +365,7 @@ export class RTCEncryptionManager {
357
365
  this.logger?.trace(`Delay Rollout for key:${newOutboundEncryptionSession.keyId}...`);
358
366
  await sleep(this.useKeyDelay);
359
367
  this.logger?.trace(`...Delayed rollout of index:${newOutboundEncryptionSession.keyId} `);
360
- this.addKeyToParticipantWithBackendIdentity(newOutboundEncryptionSession.key, newOutboundEncryptionSession.keyId, this.ownMembership, await this.getOwnRtcBackendIdentity());
368
+ for (const backendIdentity of await this.getOwnBackendIdentities()) this.addKeyToParticipantWithBackendIdentity(newOutboundEncryptionSession.key, newOutboundEncryptionSession.keyId, this.ownMembership, backendIdentity);
361
369
  }
362
370
  } catch (err) {
363
371
  this.logger?.error(`Failed to rollout key`, err);
@@ -1 +1 @@
1
- {"version":3,"file":"RTCEncryptionManager.js","names":[],"sources":["../../src/matrixrtc/RTCEncryptionManager.ts"],"sourcesContent":["/*\nCopyright 2025-2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport {\n type CallMembershipIdentityParts,\n getEncryptionKeyMapKey,\n type IEncryptionManager,\n} from \"./EncryptionManager.ts\";\nimport { type EncryptionConfig, type MembershipConfig } from \"./MatrixRTCSession.ts\";\nimport type { CallMembership } from \"./CallMembership.ts\";\nimport { decodeBase64, encodeBase64 } from \"../base64.ts\";\nimport { type IKeyTransport, type KeyTransportEventListener, KeyTransportEvents } from \"./IKeyTransport.ts\";\nimport { type Logger } from \"../logger.ts\";\nimport { sleep } from \"../utils.ts\";\nimport {\n type EncryptionKeyMapKey,\n type InboundEncryptionSession,\n type OutboundEncryptionSession,\n type ParticipantDeviceInfo,\n} from \"./types.ts\";\nimport { OutdatedKeyFilter } from \"./utils.ts\";\nimport { computeRtcIdentityRaw } from \"./membershipData/rtc.ts\";\n\n/**\n * Default for {@link EncryptionConfig.keyRotationParticipantLimit}.\n *\n * Setting this to undefined implies that we do not have a limit and full rotations are always done.\n * This is the most secure and least performant option.\n * It is highly recommended to set this to < 50 for client deployments that are planned to be used for large calls.\n * But before setting this, make yourself familiar with the exact security implications.\n * Key, rotations will stop when reaching this user limit in a call. The call will still be encrypted.\n */\nconst DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT: number | undefined = undefined;\n\n/**\n * RTCEncryptionManager is used to manage the encryption keys for a call.\n *\n * It is responsible for distributing the keys to the other participants and rotating the keys if needed.\n *\n * This manager when used with to-device transport will share the existing key only to new joiners, and rotate\n * if there is a leaver.\n *\n * XXX In the future we want to distribute a ratcheted key not the current one for new joiners.\n */\nexport class RTCEncryptionManager implements IEncryptionManager {\n // This is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n private manageMediaKeys = false;\n\n private useHashedRtcBackendIdentity = false;\n private ownRtcBackendIdentityCache: string | undefined;\n\n /**\n * Store the key rings for each participant.\n * The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.\n * The keys are stored and can be retrieved later when the application layer is ready {@link RTCEncryptionManager#getEncryptionKeys}.\n */\n private readonly participantKeyRings = new Map<\n EncryptionKeyMapKey,\n Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n rtcBackendIdentity: string;\n }>\n >();\n\n // The current per-sender media key for this device\n private outboundSession: OutboundEncryptionSession | null = null;\n\n /**\n * Ensures that there is only one distribute operation at a time for that call.\n */\n private currentKeyDistributionPromise: Promise<void> | null = null;\n\n /**\n * The time to wait before using the outbound session after it has been distributed.\n * This is to ensure that the key is delivered to all participants before it is used.\n * When creating the first key, this is set to 0 so that the key can be used immediately.\n */\n private useKeyDelay = 5000;\n\n /**\n * We want to avoid rolling out a new outbound key when the previous one was created less than `keyRotationGracePeriodMs` milliseconds ago.\n * This is to avoid expensive key rotations when users quickly join the call in a row.\n *\n * This must be higher than `useKeyDelay` to have an effect.\n * If it is lower, the current key will always be older than the grace period.\n * @private\n */\n private keyRotationGracePeriodMs = 10_000;\n\n /**\n * The number of participants at or above which we stop rotating the key altogether.\n * The current key is still distributed to new joiners, but no new key is generated.\n * @see EncryptionConfig.keyRotationParticipantLimit\n * @private\n */\n private keyRotationParticipantLimit = DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;\n\n /**\n * If a new key distribution is being requested while one is going on, we will set this flag to true.\n * This will ensure that a new round is started after the current one.\n * @private\n */\n private needToEnsureKeyAgain = false;\n\n /**\n * There is a possibility that keys arrive in the wrong order.\n * For example, after a quick join/leave/join, there will be 2 keys of index 0 distributed, and\n * if they are received in the wrong order, the stream won't be decryptable.\n * For that reason we keep a small buffer of keys for a limited time to disambiguate.\n * @private\n */\n private keyBuffer = new OutdatedKeyFilter();\n\n private logger: Logger | undefined = undefined;\n\n private readonly rtcIdentityProvider: (userId: string, deviceId: string, memberId: string) => Promise<string>;\n\n /**\n *\n * @param ownMembership - our own membership info\n * @param getMemberships - function to get current memberships\n * @param transport - key transport (room or to-device)\n * @param statistics - statistics collector\n * @param onEncryptionKeysChanged - callback to notify the media layer of new keys\n * @param parentLogger - optional parent logger\n * @param rtcBackendIdProvider - A function to compute the rtc backend identity, exposed for testing purposes\n */\n public constructor(\n private readonly ownMembership: CallMembershipIdentityParts,\n private getMemberships: () => CallMembership[],\n private transport: IKeyTransport,\n // Callback to notify the media layer of new keys\n private onEncryptionKeysChanged: (\n keyBin: Uint8Array<ArrayBuffer>,\n encryptionKeyIndex: number,\n membership: CallMembershipIdentityParts,\n rtcBackendIdentity: string,\n ) => void,\n parentLogger?: Logger,\n rtcBackendIdProvider?: (userId: string, deviceId: string, memberId: string) => Promise<string>,\n ) {\n this.logger = parentLogger?.getChild(`[EncryptionManager]`);\n this.rtcIdentityProvider = rtcBackendIdProvider ?? computeRtcIdentityRaw;\n }\n\n /**\n * Whether the session currently has too many participants for the key to be rotated.\n *\n * This is computed by checking the participant count. If there are too many participants for efficient rotations,\n * the key rotation will be suppressed.\n * While this is true, the current key is still shared with new joiners and the current call is still fully encrypted,\n * but no new key is generated for joiners or leavers. Changes are signalled by {@link MatrixRTCSessionEvent.KeyRotationSuppressedChanged}.\n * @see EncryptionConfig.keyRotationParticipantLimit\n */\n public get isKeyRotationSuppressed(): boolean {\n if (!this.manageMediaKeys) return false;\n if (this.keyRotationParticipantLimit === undefined) return false;\n return this.getMemberships().length >= this.keyRotationParticipantLimit;\n }\n\n private async getOwnRtcBackendIdentity(): Promise<string> {\n if (this.ownRtcBackendIdentityCache) return this.ownRtcBackendIdentityCache;\n\n if (this.useHashedRtcBackendIdentity) {\n const { userId, deviceId, memberId } = this.ownMembership;\n this.logger?.info(\n // If we see this log multiple times, we need to reconsider the precompute call of getOwnRtcBackendIdentity\n `Computing RTC backend identity for ${userId}:${deviceId}:${memberId} (SHOULD ONLY BE CALLED ONCE)`,\n );\n this.ownRtcBackendIdentityCache = await this.rtcIdentityProvider(userId, deviceId, memberId);\n } else {\n this.ownRtcBackendIdentityCache = `${this.ownMembership.userId}:${this.ownMembership.deviceId}`;\n }\n return this.ownRtcBackendIdentityCache;\n }\n\n public getEncryptionKeys(): ReadonlyMap<\n EncryptionKeyMapKey,\n ReadonlyArray<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n rtcBackendIdentity: string;\n }>\n > {\n return new Map(this.participantKeyRings);\n }\n\n private keysWithoutMatchingRTCMembership: Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n }> = [];\n\n private checkKeysWithoutMatchingRTCMembership(): void {\n const keyInfoTemp = this.keysWithoutMatchingRTCMembership;\n this.keysWithoutMatchingRTCMembership = [];\n keyInfoTemp.forEach((keyInfo) => {\n this.addKeyToParticipant(keyInfo.key, keyInfo.keyIndex, keyInfo.membership);\n });\n }\n\n private addKeyToParticipant(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n ): void {\n const knownRtcMembership = this.getMemberships();\n const fullMembership = knownRtcMembership.find(\n (member) => member.userId === membership.userId && member.deviceId === membership.deviceId,\n );\n if (!fullMembership) {\n this.logger?.info(\n `No matching RTC membership for key from ${membership.userId}:${membership.deviceId}, delaying key addition`,\n );\n this.keysWithoutMatchingRTCMembership.push({ key, keyIndex, membership });\n return;\n }\n this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, fullMembership.rtcBackendIdentity);\n }\n\n private addKeyToParticipantWithBackendIdentity(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n rtcBackendIdentity: string,\n ): void {\n const mapKey = getEncryptionKeyMapKey(membership);\n if (!this.participantKeyRings.has(mapKey)) {\n this.participantKeyRings.set(mapKey, []);\n }\n this.participantKeyRings.get(mapKey)!.push({ key, keyIndex, membership, rtcBackendIdentity });\n this.onEncryptionKeysChanged(key, keyIndex, membership, rtcBackendIdentity);\n }\n\n public join(joinConfig: (EncryptionConfig & MembershipConfig) | undefined): void {\n this.manageMediaKeys = joinConfig?.manageMediaKeys ?? true; // default to true\n this.useHashedRtcBackendIdentity = joinConfig?.unstableSendStickyEvents ?? false;\n this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;\n this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10_000;\n this.keyRotationParticipantLimit =\n joinConfig?.keyRotationParticipantLimit ?? DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;\n\n this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n void this.getOwnRtcBackendIdentity(); // precompute own identity\n\n this.logger?.info(`Joining room`);\n this.transport.start();\n }\n\n public leave(): void {\n this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n this.transport.stop();\n this.participantKeyRings.clear();\n }\n\n /**\n * Will ensure that a new key is distributed and used to encrypt our media.\n * If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed.\n * If this function is called repeatedly while a distribution is in progress,\n * the calls will be coalesced to a single new distribution (that will start just after the current one has completed).\n */\n private ensureKeyDistribution(): void {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) return;\n if (this.currentKeyDistributionPromise == null) {\n this.logger?.debug(`No active rollout, start a new one`);\n // start a rollout\n this.currentKeyDistributionPromise = this.rolloutOutboundKey().then(() => {\n this.logger?.debug(`Rollout completed`);\n this.currentKeyDistributionPromise = null;\n if (this.needToEnsureKeyAgain) {\n this.logger?.debug(`New Rollout needed`);\n this.needToEnsureKeyAgain = false;\n // rollout a new one\n this.ensureKeyDistribution();\n }\n });\n } else {\n // There is a rollout in progress, but a key rotation is requested (could be caused by a ownMembership change)\n // Remember that a new rotation is needed after the current one.\n this.logger?.debug(`Rollout in progress, a new rollout will be started after the current one`);\n this.needToEnsureKeyAgain = true;\n }\n }\n\n public onNewKeyReceived: KeyTransportEventListener = (membership, keyBase64Encoded, index, timestamp) => {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) {\n this.logger?.warn(\n `Received key over transport ${membership.userId}:${membership.deviceId} at index ${index} but media keys are disabled`,\n );\n return;\n }\n this.logger?.debug(`Received key over transport ${membership.userId}:${membership.deviceId} at index ${index}`);\n\n // We received a new key, notify the video layer of this new key so that it can decrypt the frames properly.\n const keyBin = decodeBase64(keyBase64Encoded);\n const candidateInboundSession: InboundEncryptionSession = {\n key: keyBin,\n membership,\n keyIndex: index,\n creationTS: timestamp,\n };\n\n const outdated = this.keyBuffer.isOutdated(membership, candidateInboundSession);\n if (!outdated) {\n this.addKeyToParticipant(\n candidateInboundSession.key,\n candidateInboundSession.keyIndex,\n candidateInboundSession.membership,\n );\n } else {\n this.logger?.info(\n `Received an out of order key for ${membership.userId}:${membership.deviceId}, dropping it`,\n );\n }\n };\n\n /**\n * Called when the ownMembership of the call changes.\n * This encryption manager is very basic, it will rotate the key everytime this is called.\n * @param oldMemberships - This parameter is not used here, but it is kept for compatibility with the interface.\n */\n public onMembershipsUpdate(oldMemberships: CallMembership[] = []): void {\n this.logger?.trace(`onMembershipsUpdate`);\n\n // Ensure the key is distributed. This will be no-op if the key is already being distributed to everyone.\n // If there is an ongoing distribution, it will be completed before a new one is started.\n this.ensureKeyDistribution();\n // ensure key emission to the rtc backend\n this.checkKeysWithoutMatchingRTCMembership();\n }\n\n private async rolloutOutboundKey(): Promise<void> {\n const isFirstKey = this.outboundSession == null;\n if (isFirstKey) {\n // create the first key\n const firstKey = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: 0,\n };\n this.outboundSession = firstKey;\n this.addKeyToParticipantWithBackendIdentity(\n firstKey.key,\n firstKey.keyId,\n this.ownMembership,\n await this.getOwnRtcBackendIdentity(),\n );\n }\n // get current memberships\n const toShareWith: ParticipantDeviceInfo[] = this.getMemberships()\n .filter((membership) => {\n return membership.sender != undefined;\n })\n .map((membership) => {\n return {\n userId: membership.sender,\n deviceId: membership.deviceId,\n membershipTs: membership.createdTs(),\n };\n });\n\n let alreadySharedWith = this.outboundSession?.sharedWith ?? [];\n\n // Some users might have rotate their ownMembership event (formally called fingerprint) meaning they might have\n // clear their key. Reset the `alreadySharedWith` flag for them.\n alreadySharedWith = alreadySharedWith.filter(\n (x) =>\n // If there was a member with same userId and deviceId but different membershipTs, we need to clear it\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs != o.membershipTs,\n ),\n );\n\n const anyLeft = alreadySharedWith.filter(\n (x) =>\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n const anyJoined = toShareWith.filter(\n (x) =>\n !alreadySharedWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n\n let toDistributeTo: ParticipantDeviceInfo[] = [];\n //default to current session\n let newOutboundEncryptionSession: OutboundEncryptionSession = this.outboundSession!;\n let hasKeyChanged = false;\n\n // Rotating means sending the new key to every participant, this is expensive in large session.\n if (this.isKeyRotationSuppressed) {\n if (anyJoined.length > 0) {\n this.logger?.debug(\n `New joiners detected, but the session has ${toShareWith.length} participants (limit:${this.keyRotationParticipantLimit}), keeping the key`,\n );\n }\n toDistributeTo = anyJoined;\n } else {\n if (anyLeft.length > 0) {\n // We need to rotate the key\n newOutboundEncryptionSession = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n } else if (anyJoined.length > 0) {\n const keyAge = Date.now() - this.outboundSession!.creationTS;\n if (keyAge < this.keyRotationGracePeriodMs) {\n this.logger?.debug(\n `New joiners detected, but the key is recent enough (age:${keyAge}), keeping it`,\n );\n toDistributeTo = anyJoined;\n } else {\n this.logger?.debug(`New joiners detected, rotating the key`);\n // We need to rotate the key\n newOutboundEncryptionSession = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n }\n }\n }\n // return early if we dont have anything to distribute.\n if (toDistributeTo.length === 0) {\n return;\n }\n\n try {\n this.logger?.trace(`Sending key...`);\n await this.transport.sendKey(\n encodeBase64(newOutboundEncryptionSession.key),\n newOutboundEncryptionSession.keyId,\n toDistributeTo,\n );\n newOutboundEncryptionSession.sharedWith.push(...toDistributeTo);\n const outboundSessionList = newOutboundEncryptionSession.sharedWith\n .map((m) => `${m.userId}:${m.deviceId}`)\n .join(\",\");\n this.logger?.trace(`key index:${newOutboundEncryptionSession.keyId} sent to ${outboundSessionList}`);\n if (hasKeyChanged) {\n // Delay a bit before using this key\n // It is recommended not to start using a key immediately but instead wait for a short time to make sure it is delivered.\n this.logger?.trace(`Delay Rollout for key:${newOutboundEncryptionSession.keyId}...`);\n await sleep(this.useKeyDelay);\n this.logger?.trace(`...Delayed rollout of index:${newOutboundEncryptionSession.keyId} `);\n this.addKeyToParticipantWithBackendIdentity(\n newOutboundEncryptionSession.key,\n newOutboundEncryptionSession.keyId,\n this.ownMembership,\n await this.getOwnRtcBackendIdentity(),\n );\n }\n } catch (err) {\n this.logger?.error(`Failed to rollout key`, err);\n }\n }\n\n private createNewOutboundSession(): OutboundEncryptionSession {\n const newOutboundKey: OutboundEncryptionSession = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: this.nextKeyIndex(),\n };\n\n this.logger?.info(`creating new outbound key index:${newOutboundKey.keyId}`);\n // Set this new key as the current one\n this.outboundSession = newOutboundKey;\n return newOutboundKey;\n }\n\n private nextKeyIndex(): number {\n if (this.outboundSession) {\n return (this.outboundSession.keyId + 1) % 256;\n }\n return 0;\n }\n\n private generateRandomKey(): Uint8Array<ArrayBuffer> {\n const key = new Uint8Array(16);\n globalThis.crypto.getRandomValues(key);\n return key;\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"RTCEncryptionManager.js","names":[],"sources":["../../src/matrixrtc/RTCEncryptionManager.ts"],"sourcesContent":["/*\nCopyright 2025-2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport {\n type CallMembershipIdentityParts,\n getEncryptionKeyMapKey,\n type IEncryptionManager,\n} from \"./EncryptionManager.ts\";\nimport { type EncryptionConfig } from \"./MatrixRTCSession.ts\";\nimport type { CallMembership } from \"./CallMembership.ts\";\nimport { decodeBase64, encodeBase64 } from \"../base64.ts\";\nimport { type IKeyTransport, type KeyTransportEventListener, KeyTransportEvents } from \"./IKeyTransport.ts\";\nimport { type Logger } from \"../logger.ts\";\nimport { sleep } from \"../utils.ts\";\nimport {\n type Transport,\n type EncryptionKeyMapKey,\n type InboundEncryptionSession,\n type OutboundEncryptionSession,\n type ParticipantDeviceInfo,\n} from \"./types.ts\";\nimport { OutdatedKeyFilter } from \"./utils.ts\";\nimport { computeRtcIdentityRaw } from \"./membershipData/rtc.ts\";\nimport { isUnstableLivekitTransport } from \"./LivekitTransport.ts\";\n\n/**\n * Default for {@link EncryptionConfig.keyRotationParticipantLimit}.\n *\n * Setting this to undefined implies that we do not have a limit and full rotations are always done.\n * This is the most secure and least performant option.\n * It is highly recommended to set this to < 50 for client deployments that are planned to be used for large calls.\n * But before setting this, make yourself familiar with the exact security implications.\n * Key, rotations will stop when reaching this user limit in a call. The call will still be encrypted.\n */\nconst DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT: number | undefined = undefined;\n\n/**\n * RTCEncryptionManager is used to manage the encryption keys for a call.\n *\n * It is responsible for distributing the keys to the other participants and rotating the keys if needed.\n *\n * This manager when used with to-device transport will share the existing key only to new joiners, and rotate\n * if there is a leaver.\n *\n * XXX In the future we want to distribute a ratcheted key not the current one for new joiners.\n */\nexport class RTCEncryptionManager implements IEncryptionManager {\n // This is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n private manageMediaKeys = false;\n\n private publishedTransports: Transport[] = [];\n\n /**\n * Store the key rings for each participant.\n * The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.\n * The keys are stored and can be retrieved later when the application layer is ready {@link RTCEncryptionManager#getEncryptionKeys}.\n */\n private readonly participantKeyRings = new Map<\n EncryptionKeyMapKey,\n Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n backendIdentity: string;\n }>\n >();\n\n // The current per-sender media key for this device\n private outboundSession: OutboundEncryptionSession | null = null;\n\n /**\n * Ensures that there is only one distribute operation at a time for that call.\n */\n private currentKeyDistributionPromise: Promise<void> | null = null;\n\n /**\n * The time to wait before using the outbound session after it has been distributed.\n * This is to ensure that the key is delivered to all participants before it is used.\n * When creating the first key, this is set to 0 so that the key can be used immediately.\n */\n private useKeyDelay = 5000;\n\n /**\n * We want to avoid rolling out a new outbound key when the previous one was created less than `keyRotationGracePeriodMs` milliseconds ago.\n * This is to avoid expensive key rotations when users quickly join the call in a row.\n *\n * This must be higher than `useKeyDelay` to have an effect.\n * If it is lower, the current key will always be older than the grace period.\n * @private\n */\n private keyRotationGracePeriodMs = 10_000;\n\n /**\n * The number of participants at or above which we stop rotating the key altogether.\n * The current key is still distributed to new joiners, but no new key is generated.\n * @see EncryptionConfig.keyRotationParticipantLimit\n * @private\n */\n private keyRotationParticipantLimit = DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;\n\n /**\n * If a new key distribution is being requested while one is going on, we will set this flag to true.\n * This will ensure that a new round is started after the current one.\n * @private\n */\n private needToEnsureKeyAgain = false;\n\n /**\n * There is a possibility that keys arrive in the wrong order.\n * For example, after a quick join/leave/join, there will be 2 keys of index 0 distributed, and\n * if they are received in the wrong order, the stream won't be decryptable.\n * For that reason we keep a small buffer of keys for a limited time to disambiguate.\n * @private\n */\n private keyBuffer = new OutdatedKeyFilter();\n\n private logger: Logger | undefined = undefined;\n\n private readonly rtcIdentityProvider: (userId: string, memberId: string) => Promise<string>;\n\n /**\n *\n * @param ownMembership - our own membership info\n * @param getMemberships - function to get current memberships\n * @param transport - key transport (room or to-device)\n * @param statistics - statistics collector\n * @param onEncryptionKeysChanged - callback to notify the media layer of new keys\n * @param parentLogger - optional parent logger\n * @param rtcBackendIdProvider - A function to compute the rtc backend identity, exposed for testing purposes\n */\n public constructor(\n private readonly ownMembership: CallMembershipIdentityParts,\n private getMemberships: () => CallMembership[],\n private transport: IKeyTransport,\n // Callback to notify the media layer of new keys\n private onEncryptionKeysChanged: (\n keyBin: Uint8Array<ArrayBuffer>,\n encryptionKeyIndex: number,\n membership: CallMembershipIdentityParts,\n backendIdentity: string,\n ) => void,\n parentLogger?: Logger,\n rtcBackendIdProvider?: (userId: string, memberId: string) => Promise<string>,\n ) {\n this.logger = parentLogger?.getChild(`[EncryptionManager]`);\n this.rtcIdentityProvider = rtcBackendIdProvider ?? computeRtcIdentityRaw;\n }\n\n /**\n * Whether the session currently has too many participants for the key to be rotated.\n *\n * This is computed by checking the participant count. If there are too many participants for efficient rotations,\n * the key rotation will be suppressed.\n * While this is true, the current key is still shared with new joiners and the current call is still fully encrypted,\n * but no new key is generated for joiners or leavers. Changes are signalled by {@link MatrixRTCSessionEvent.KeyRotationSuppressedChanged}.\n * @see EncryptionConfig.keyRotationParticipantLimit\n */\n public get isKeyRotationSuppressed(): boolean {\n if (!this.manageMediaKeys) return false;\n if (this.keyRotationParticipantLimit === undefined) return false;\n return this.getMemberships().length >= this.keyRotationParticipantLimit;\n }\n\n private ownBackendIdentitiesPromise: Promise<string[]> | undefined;\n\n private getOwnBackendIdentities(): Promise<string[]> {\n this.ownBackendIdentitiesPromise ??= (async (): Promise<string[]> => {\n const { userId, deviceId, memberId } = this.ownMembership;\n const livekitTransports = this.publishedTransports.filter(isUnstableLivekitTransport);\n const backendIdentities: string[] = [];\n\n if (livekitTransports.some((t) => \"url\" in t)) {\n this.logger?.info(`Computing RTC backend identity for ${userId}:${deviceId}:${memberId}`);\n backendIdentities.push(await this.rtcIdentityProvider(userId, memberId));\n }\n if (livekitTransports.some((t) => \"livekit_service_url\" in t)) {\n backendIdentities.push(`${userId}:${deviceId}`);\n }\n\n return backendIdentities;\n })();\n return this.ownBackendIdentitiesPromise;\n }\n\n public getEncryptionKeys(): ReadonlyMap<\n EncryptionKeyMapKey,\n ReadonlyArray<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n backendIdentity: string;\n }>\n > {\n return new Map(this.participantKeyRings);\n }\n\n private keysWithoutMatchingRTCMembership: Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n }> = [];\n\n private checkKeysWithoutMatchingRTCMembership(): void {\n const keyInfoTemp = this.keysWithoutMatchingRTCMembership;\n this.keysWithoutMatchingRTCMembership = [];\n keyInfoTemp.forEach((keyInfo) => {\n this.addKeyToParticipant(keyInfo.key, keyInfo.keyIndex, keyInfo.membership);\n });\n }\n\n private addKeyToParticipant(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n ): void {\n const knownRtcMembership = this.getMemberships();\n const fullMembership = knownRtcMembership.find(\n (member) => member.userId === membership.userId && member.deviceId === membership.deviceId,\n );\n if (!fullMembership) {\n this.logger?.info(\n `No matching RTC membership for key from ${membership.userId}:${membership.deviceId}, delaying key addition`,\n );\n this.keysWithoutMatchingRTCMembership.push({ key, keyIndex, membership });\n return;\n }\n for (const backendIdentity of fullMembership.backendIdentities) {\n this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, backendIdentity);\n }\n }\n\n private addKeyToParticipantWithBackendIdentity(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n backendIdentity: string,\n ): void {\n const mapKey = getEncryptionKeyMapKey(membership);\n if (!this.participantKeyRings.has(mapKey)) {\n this.participantKeyRings.set(mapKey, []);\n }\n this.participantKeyRings.get(mapKey)!.push({ key, keyIndex, membership, backendIdentity });\n this.onEncryptionKeysChanged(key, keyIndex, membership, backendIdentity);\n }\n\n public join(joinConfig: EncryptionConfig | undefined, publishedTransports: Transport[]): void {\n this.manageMediaKeys = joinConfig?.manageMediaKeys ?? true; // default to true\n this.publishedTransports = publishedTransports;\n this.ownBackendIdentitiesPromise = undefined;\n this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;\n this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10_000;\n this.keyRotationParticipantLimit =\n joinConfig?.keyRotationParticipantLimit ?? DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;\n\n this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n void this.getOwnBackendIdentities(); // precompute own identity\n\n this.logger?.info(`Joining room`);\n this.transport.start();\n }\n\n public leave(): void {\n this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n this.transport.stop();\n this.participantKeyRings.clear();\n }\n\n /**\n * Will ensure that a new key is distributed and used to encrypt our media.\n * If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed.\n * If this function is called repeatedly while a distribution is in progress,\n * the calls will be coalesced to a single new distribution (that will start just after the current one has completed).\n */\n private ensureKeyDistribution(): void {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) return;\n if (this.currentKeyDistributionPromise == null) {\n this.logger?.debug(`No active rollout, start a new one`);\n // start a rollout\n this.currentKeyDistributionPromise = this.rolloutOutboundKey().then(() => {\n this.logger?.debug(`Rollout completed`);\n this.currentKeyDistributionPromise = null;\n if (this.needToEnsureKeyAgain) {\n this.logger?.debug(`New Rollout needed`);\n this.needToEnsureKeyAgain = false;\n // rollout a new one\n this.ensureKeyDistribution();\n }\n });\n } else {\n // There is a rollout in progress, but a key rotation is requested (could be caused by a ownMembership change)\n // Remember that a new rotation is needed after the current one.\n this.logger?.debug(`Rollout in progress, a new rollout will be started after the current one`);\n this.needToEnsureKeyAgain = true;\n }\n }\n\n public onNewKeyReceived: KeyTransportEventListener = (membership, keyBase64Encoded, index, timestamp) => {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) {\n this.logger?.warn(\n `Received key over transport ${membership.userId}:${membership.deviceId} at index ${index} but media keys are disabled`,\n );\n return;\n }\n this.logger?.debug(`Received key over transport ${membership.userId}:${membership.deviceId} at index ${index}`);\n\n // We received a new key, notify the video layer of this new key so that it can decrypt the frames properly.\n const keyBin = decodeBase64(keyBase64Encoded);\n const candidateInboundSession: InboundEncryptionSession = {\n key: keyBin,\n membership,\n keyIndex: index,\n creationTS: timestamp,\n };\n\n const outdated = this.keyBuffer.isOutdated(membership, candidateInboundSession);\n if (!outdated) {\n this.addKeyToParticipant(\n candidateInboundSession.key,\n candidateInboundSession.keyIndex,\n candidateInboundSession.membership,\n );\n } else {\n this.logger?.info(\n `Received an out of order key for ${membership.userId}:${membership.deviceId}, dropping it`,\n );\n }\n };\n\n /**\n * Called when the ownMembership of the call changes.\n * This encryption manager is very basic, it will rotate the key everytime this is called.\n * @param oldMemberships - This parameter is not used here, but it is kept for compatibility with the interface.\n */\n public onMembershipsUpdate(oldMemberships: CallMembership[] = []): void {\n this.logger?.trace(`onMembershipsUpdate`);\n\n // Ensure the key is distributed. This will be no-op if the key is already being distributed to everyone.\n // If there is an ongoing distribution, it will be completed before a new one is started.\n this.ensureKeyDistribution();\n // ensure key emission to the rtc backend\n this.checkKeysWithoutMatchingRTCMembership();\n }\n\n private async rolloutOutboundKey(): Promise<void> {\n const isFirstKey = this.outboundSession == null;\n if (isFirstKey) {\n // create the first key\n const firstKey = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: 0,\n };\n this.outboundSession = firstKey;\n const ids = await this.getOwnBackendIdentities();\n for (const backendIdentity of ids)\n this.addKeyToParticipantWithBackendIdentity(\n firstKey.key,\n firstKey.keyId,\n this.ownMembership,\n backendIdentity,\n );\n }\n // get current memberships\n const toShareWith: ParticipantDeviceInfo[] = this.getMemberships()\n .filter((membership) => {\n return membership.sender != undefined;\n })\n .map((membership) => {\n return {\n userId: membership.sender,\n deviceId: membership.deviceId,\n membershipTs: membership.createdTs(),\n };\n });\n\n let alreadySharedWith = this.outboundSession?.sharedWith ?? [];\n\n // Some users might have rotate their ownMembership event (formally called fingerprint) meaning they might have\n // clear their key. Reset the `alreadySharedWith` flag for them.\n alreadySharedWith = alreadySharedWith.filter(\n (x) =>\n // If there was a member with same userId and deviceId but different membershipTs, we need to clear it\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs != o.membershipTs,\n ),\n );\n\n const anyLeft = alreadySharedWith.filter(\n (x) =>\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n const anyJoined = toShareWith.filter(\n (x) =>\n !alreadySharedWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n\n let toDistributeTo: ParticipantDeviceInfo[] = [];\n //default to current session\n let newOutboundEncryptionSession: OutboundEncryptionSession = this.outboundSession!;\n let hasKeyChanged = false;\n\n // Rotating means sending the new key to every participant, this is expensive in large session.\n if (this.isKeyRotationSuppressed) {\n if (anyJoined.length > 0) {\n this.logger?.debug(\n `New joiners detected, but the session has ${toShareWith.length} participants (limit:${this.keyRotationParticipantLimit}), keeping the key`,\n );\n }\n toDistributeTo = anyJoined;\n } else {\n if (anyLeft.length > 0) {\n // We need to rotate the key\n newOutboundEncryptionSession = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n } else if (anyJoined.length > 0) {\n const keyAge = Date.now() - this.outboundSession!.creationTS;\n if (keyAge < this.keyRotationGracePeriodMs) {\n this.logger?.debug(\n `New joiners detected, but the key is recent enough (age:${keyAge}), keeping it`,\n );\n toDistributeTo = anyJoined;\n } else {\n this.logger?.debug(`New joiners detected, rotating the key`);\n // We need to rotate the key\n newOutboundEncryptionSession = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n }\n }\n }\n // return early if we dont have anything to distribute.\n if (toDistributeTo.length === 0) {\n return;\n }\n\n try {\n this.logger?.trace(`Sending key...`);\n await this.transport.sendKey(\n encodeBase64(newOutboundEncryptionSession.key),\n newOutboundEncryptionSession.keyId,\n toDistributeTo,\n );\n newOutboundEncryptionSession.sharedWith.push(...toDistributeTo);\n const outboundSessionList = newOutboundEncryptionSession.sharedWith\n .map((m) => `${m.userId}:${m.deviceId}`)\n .join(\",\");\n this.logger?.trace(`key index:${newOutboundEncryptionSession.keyId} sent to ${outboundSessionList}`);\n if (hasKeyChanged) {\n // Delay a bit before using this key\n // It is recommended not to start using a key immediately but instead wait for a short time to make sure it is delivered.\n this.logger?.trace(`Delay Rollout for key:${newOutboundEncryptionSession.keyId}...`);\n await sleep(this.useKeyDelay);\n this.logger?.trace(`...Delayed rollout of index:${newOutboundEncryptionSession.keyId} `);\n for (const backendIdentity of await this.getOwnBackendIdentities())\n this.addKeyToParticipantWithBackendIdentity(\n newOutboundEncryptionSession.key,\n newOutboundEncryptionSession.keyId,\n this.ownMembership,\n backendIdentity,\n );\n }\n } catch (err) {\n this.logger?.error(`Failed to rollout key`, err);\n }\n }\n\n private createNewOutboundSession(): OutboundEncryptionSession {\n const newOutboundKey: OutboundEncryptionSession = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: this.nextKeyIndex(),\n };\n\n this.logger?.info(`creating new outbound key index:${newOutboundKey.keyId}`);\n // Set this new key as the current one\n this.outboundSession = newOutboundKey;\n return newOutboundKey;\n }\n\n private nextKeyIndex(): number {\n if (this.outboundSession) {\n return (this.outboundSession.keyId + 1) % 256;\n }\n return 0;\n }\n\n private generateRandomKey(): Uint8Array<ArrayBuffer> {\n const key = new Uint8Array(16);\n globalThis.crypto.getRandomValues(key);\n return key;\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -45,14 +45,13 @@ export class ToDeviceKeyTransport extends TypedEventEmitter {
45
45
  return;
46
46
  }
47
47
 
48
- // TODO: Not possible to check if the event is encrypted or not
49
- // see https://github.com/matrix-org/matrix-rust-sdk/issues/4883
50
- // if (evnt.getWireType() != EventType.RoomMessageEncrypted) {
51
- // // WARN: The call keys were sent in clear. Ignore them
52
- // logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`);
53
- // return;
54
- // }
55
-
48
+ // NB: When received via the widget driver, the to-device events
49
+ // are properly reconstructed as if they are encrypted (see MatrixEvent#makeEncrypted).
50
+ if (event.getWireType() != EventType.RoomMessageEncrypted) {
51
+ // WARN: The call keys were sent in clear. Ignore them
52
+ this.logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`);
53
+ return;
54
+ }
56
55
  const content = this.getValidEventContent(event);
57
56
  if (!content) return;
58
57
  if (!event.getSender()) return;
@@ -1 +1 @@
1
- {"version":3,"file":"ToDeviceKeyTransport.js","names":[],"sources":["../../src/matrixrtc/ToDeviceKeyTransport.ts"],"sourcesContent":["/*\nCopyright 2025 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { type WidgetApiResponseError } from \"matrix-widget-api\";\n\nimport { TypedEventEmitter } from \"../models/typed-event-emitter.ts\";\nimport { type IKeyTransport, KeyTransportEvents, type KeyTransportEventsHandlerMap } from \"./IKeyTransport.ts\";\nimport { type Logger, logger as rootLogger } from \"../logger.ts\";\nimport { type EncryptionKeysToDeviceEventContent, type ParticipantDeviceInfo, type Statistics } from \"./types.ts\";\nimport { ClientEvent, type MatrixClient } from \"../client.ts\";\nimport type { MatrixEvent } from \"../models/event.ts\";\nimport { EventType } from \"../@types/event.ts\";\nimport { type CallMembershipIdentityParts } from \"./EncryptionManager.ts\";\n\nexport class NotSupportedError extends Error {\n public constructor(message?: string) {\n super(message);\n }\n public get name(): string {\n return \"NotSupportedError\";\n }\n}\n/**\n * ToDeviceKeyTransport is used to send MatrixRTC keys to other devices using the\n * to-device CS-API.\n */\nexport class ToDeviceKeyTransport\n extends TypedEventEmitter<KeyTransportEvents, KeyTransportEventsHandlerMap>\n implements IKeyTransport\n{\n private logger: Logger = rootLogger;\n\n public setParentLogger(parentLogger: Logger): void {\n this.logger = parentLogger.getChild(`[ToDeviceKeyTransport]`);\n }\n\n public constructor(\n private membership: CallMembershipIdentityParts,\n private roomId: string,\n private client: Pick<MatrixClient, \"encryptAndSendToDevice\" | \"on\" | \"off\">,\n private statistics: Statistics,\n parentLogger?: Logger,\n ) {\n super();\n this.setParentLogger(parentLogger ?? rootLogger);\n }\n\n public start(): void {\n this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);\n }\n\n public stop(): void {\n this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);\n }\n\n public async sendKey(keyBase64Encoded: string, index: number, members: ParticipantDeviceInfo[]): Promise<void> {\n const content: EncryptionKeysToDeviceEventContent = {\n keys: {\n index: index,\n key: keyBase64Encoded,\n },\n room_id: this.roomId,\n member: {\n claimed_device_id: this.membership.deviceId,\n id: this.membership.memberId,\n },\n session: {\n call_id: \"\",\n application: \"m.call\",\n scope: \"m.room\",\n },\n sent_ts: Date.now(),\n };\n\n const targets = members\n .map((member) => {\n return {\n userId: member.userId,\n deviceId: member.deviceId,\n };\n })\n // filter out me\n .filter(\n (member) => !(member.userId == this.membership.userId && member.deviceId == this.membership.deviceId),\n );\n\n if (targets.length > 0) {\n await this.client\n .encryptAndSendToDevice(EventType.CallEncryptionKeysPrefix, targets, content)\n .catch((error: WidgetApiResponseError) => {\n const msg: string = error.message;\n // This is not ideal. We would want to have a custom error type for unsupported actions.\n // This is not part of the widget API spec. Since as of now there are only two implementations:\n // Rust SDK + JS-SDK, and the JS-SDK does support to-device sending, we can assume that\n // this is a widget driver issue error message.\n if (\n (msg.includes(\"unknown variant\") && msg.includes(\"send_to_device\")) ||\n msg.includes(\"not supported\")\n ) {\n throw new NotSupportedError(\"The widget driver does not support to-device encryption\");\n }\n });\n this.statistics.counters.roomEventEncryptionKeysSent += 1;\n } else {\n this.logger.warn(\"No targets found for sending key\");\n }\n }\n\n private receiveCallKeyEvent(fromUser: string, content: EncryptionKeysToDeviceEventContent): void {\n // The event has already been validated at this point.\n\n this.statistics.counters.roomEventEncryptionKeysReceived += 1;\n\n // What is this, and why is it needed?\n // Also to device events do not have an origin server ts\n const now = Date.now();\n const age = now - (typeof content.sent_ts === \"number\" ? content.sent_ts : now);\n this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;\n\n const hardcodedMemberIdAlternative = `${fromUser}:${content.member.claimed_device_id}`;\n\n this.emit(\n KeyTransportEvents.ReceivedKeys,\n // TODO userId this is claimed information, deviceId is claimed information\n {\n userId: fromUser,\n deviceId: content.member.claimed_device_id,\n memberId: content.member.id ?? hardcodedMemberIdAlternative,\n },\n content.keys.key,\n content.keys.index,\n now,\n );\n }\n\n private onToDeviceEvent = (event: MatrixEvent): void => {\n if (event.getType() !== EventType.CallEncryptionKeysPrefix) {\n // Ignore this is not a call encryption event\n return;\n }\n\n // TODO: Not possible to check if the event is encrypted or not\n // see https://github.com/matrix-org/matrix-rust-sdk/issues/4883\n // if (evnt.getWireType() != EventType.RoomMessageEncrypted) {\n // // WARN: The call keys were sent in clear. Ignore them\n // logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`);\n // return;\n // }\n\n const content = this.getValidEventContent(event);\n if (!content) return;\n\n if (!event.getSender()) return;\n\n this.receiveCallKeyEvent(event.getSender()!, content);\n };\n\n private getValidEventContent(event: MatrixEvent): EncryptionKeysToDeviceEventContent | undefined {\n const content = event.getContent();\n const roomId = content.room_id;\n if (!roomId) {\n // Invalid event\n this.logger.warn(\"Malformed Event: invalid call encryption keys event, no roomId\");\n return;\n }\n if (roomId !== this.roomId) {\n this.logger.warn(\"Malformed Event: Mismatch roomId\");\n return;\n }\n\n if (!content.keys || !content.keys.key || typeof content.keys.index !== \"number\") {\n this.logger.warn(\"Malformed Event: Missing keys field\");\n return;\n }\n\n if (!content.member || !content.member.claimed_device_id) {\n this.logger.warn(\"Malformed Event: Missing claimed_device_id\");\n return;\n }\n\n // TODO check for session related fields once the to-device encryption uses the new format.\n return content as EncryptionKeysToDeviceEventContent;\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"ToDeviceKeyTransport.js","names":[],"sources":["../../src/matrixrtc/ToDeviceKeyTransport.ts"],"sourcesContent":["/*\nCopyright 2025 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { type WidgetApiResponseError } from \"matrix-widget-api\";\n\nimport { TypedEventEmitter } from \"../models/typed-event-emitter.ts\";\nimport { type IKeyTransport, KeyTransportEvents, type KeyTransportEventsHandlerMap } from \"./IKeyTransport.ts\";\nimport { type Logger, logger as rootLogger } from \"../logger.ts\";\nimport { type EncryptionKeysToDeviceEventContent, type ParticipantDeviceInfo, type Statistics } from \"./types.ts\";\nimport { ClientEvent, type MatrixClient } from \"../client.ts\";\nimport type { MatrixEvent } from \"../models/event.ts\";\nimport { EventType } from \"../@types/event.ts\";\nimport { type CallMembershipIdentityParts } from \"./EncryptionManager.ts\";\n\nexport class NotSupportedError extends Error {\n public constructor(message?: string) {\n super(message);\n }\n public get name(): string {\n return \"NotSupportedError\";\n }\n}\n/**\n * ToDeviceKeyTransport is used to send MatrixRTC keys to other devices using the\n * to-device CS-API.\n */\nexport class ToDeviceKeyTransport\n extends TypedEventEmitter<KeyTransportEvents, KeyTransportEventsHandlerMap>\n implements IKeyTransport\n{\n private logger: Logger = rootLogger;\n\n public setParentLogger(parentLogger: Logger): void {\n this.logger = parentLogger.getChild(`[ToDeviceKeyTransport]`);\n }\n\n public constructor(\n private membership: CallMembershipIdentityParts,\n private roomId: string,\n private client: Pick<MatrixClient, \"encryptAndSendToDevice\" | \"on\" | \"off\">,\n private statistics: Statistics,\n parentLogger?: Logger,\n ) {\n super();\n this.setParentLogger(parentLogger ?? rootLogger);\n }\n\n public start(): void {\n this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);\n }\n\n public stop(): void {\n this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);\n }\n\n public async sendKey(keyBase64Encoded: string, index: number, members: ParticipantDeviceInfo[]): Promise<void> {\n const content: EncryptionKeysToDeviceEventContent = {\n keys: {\n index: index,\n key: keyBase64Encoded,\n },\n room_id: this.roomId,\n member: {\n claimed_device_id: this.membership.deviceId,\n id: this.membership.memberId,\n },\n session: {\n call_id: \"\",\n application: \"m.call\",\n scope: \"m.room\",\n },\n sent_ts: Date.now(),\n };\n\n const targets = members\n .map((member) => {\n return {\n userId: member.userId,\n deviceId: member.deviceId,\n };\n })\n // filter out me\n .filter(\n (member) => !(member.userId == this.membership.userId && member.deviceId == this.membership.deviceId),\n );\n\n if (targets.length > 0) {\n await this.client\n .encryptAndSendToDevice(EventType.CallEncryptionKeysPrefix, targets, content)\n .catch((error: WidgetApiResponseError) => {\n const msg: string = error.message;\n // This is not ideal. We would want to have a custom error type for unsupported actions.\n // This is not part of the widget API spec. Since as of now there are only two implementations:\n // Rust SDK + JS-SDK, and the JS-SDK does support to-device sending, we can assume that\n // this is a widget driver issue error message.\n if (\n (msg.includes(\"unknown variant\") && msg.includes(\"send_to_device\")) ||\n msg.includes(\"not supported\")\n ) {\n throw new NotSupportedError(\"The widget driver does not support to-device encryption\");\n }\n });\n this.statistics.counters.roomEventEncryptionKeysSent += 1;\n } else {\n this.logger.warn(\"No targets found for sending key\");\n }\n }\n\n private receiveCallKeyEvent(fromUser: string, content: EncryptionKeysToDeviceEventContent): void {\n // The event has already been validated at this point.\n\n this.statistics.counters.roomEventEncryptionKeysReceived += 1;\n\n // What is this, and why is it needed?\n // Also to device events do not have an origin server ts\n const now = Date.now();\n const age = now - (typeof content.sent_ts === \"number\" ? content.sent_ts : now);\n this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;\n\n const hardcodedMemberIdAlternative = `${fromUser}:${content.member.claimed_device_id}`;\n\n this.emit(\n KeyTransportEvents.ReceivedKeys,\n // TODO userId this is claimed information, deviceId is claimed information\n {\n userId: fromUser,\n deviceId: content.member.claimed_device_id,\n memberId: content.member.id ?? hardcodedMemberIdAlternative,\n },\n content.keys.key,\n content.keys.index,\n now,\n );\n }\n\n private onToDeviceEvent = (event: MatrixEvent): void => {\n if (event.getType() !== EventType.CallEncryptionKeysPrefix) {\n // Ignore this is not a call encryption event\n return;\n }\n\n // NB: When received via the widget driver, the to-device events\n // are properly reconstructed as if they are encrypted (see MatrixEvent#makeEncrypted).\n if (event.getWireType() != EventType.RoomMessageEncrypted) {\n // WARN: The call keys were sent in clear. Ignore them\n this.logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`);\n return;\n }\n\n const content = this.getValidEventContent(event);\n if (!content) return;\n\n if (!event.getSender()) return;\n\n this.receiveCallKeyEvent(event.getSender()!, content);\n };\n\n private getValidEventContent(event: MatrixEvent): EncryptionKeysToDeviceEventContent | undefined {\n const content = event.getContent();\n const roomId = content.room_id;\n if (!roomId) {\n // Invalid event\n this.logger.warn(\"Malformed Event: invalid call encryption keys event, no roomId\");\n return;\n }\n if (roomId !== this.roomId) {\n this.logger.warn(\"Malformed Event: Mismatch roomId\");\n return;\n }\n\n if (!content.keys || !content.keys.key || typeof content.keys.index !== \"number\") {\n this.logger.warn(\"Malformed Event: Missing keys field\");\n return;\n }\n\n if (!content.member || !content.member.claimed_device_id) {\n this.logger.warn(\"Malformed Event: Missing claimed_device_id\");\n return;\n }\n\n // TODO check for session related fields once the to-device encryption uses the new format.\n return content as EncryptionKeysToDeviceEventContent;\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,28 +1,23 @@
1
1
  import type { IContent } from "../../models/event.ts";
2
- import type { RelationType } from "../../types.ts";
3
2
  import { type RtcSlotApplicationContent, type Transport } from "../types.ts";
4
3
  /**
5
4
  * Represents the current form of MSC4143, which uses sticky events to store membership.
6
5
  */
7
6
  export interface RtcMembershipData {
8
- "slot_id": string;
9
- "member": {
7
+ slot_id: string;
8
+ member: {
10
9
  user_id: string;
11
10
  device_id: string;
12
11
  id: string;
13
12
  };
14
- "m.relates_to"?: {
15
- event_id: string;
16
- rel_type: RelationType.Reference;
17
- };
18
- "application": RtcSlotApplicationContent;
19
- "transports": {
13
+ application: RtcSlotApplicationContent;
14
+ transports: {
20
15
  published: Transport[];
21
16
  can_subscribe: string[];
22
17
  };
23
- "versions": string[];
24
- "msc4354_sticky_key"?: string;
25
- "sticky_key"?: string;
18
+ versions: string[];
19
+ msc4354_sticky_key?: string;
20
+ sticky_key?: string;
26
21
  }
27
22
  /**
28
23
  * Validates that `data` matches the format expected by MSC4143.
@@ -32,5 +27,5 @@ export interface RtcMembershipData {
32
27
  * @throws {MatrixRTCMembershipParseError} if the content is not valid
33
28
  */
34
29
  export declare const checkRtcMembershipData: (data: IContent, sender: string) => data is RtcMembershipData;
35
- export declare function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string>;
30
+ export declare function computeRtcIdentityRaw(userId: string, memberId: string): Promise<string>;
36
31
  //# sourceMappingURL=rtc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rtc.d.ts","sourceRoot":"","sources":["../../../src/matrixrtc/membershipData/rtc.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,KAAK,yBAAyB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7E;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;KACd,CAAC;IACF,cAAc,CAAC,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC;KACpC,CAAC;IACF,aAAa,EAAE,yBAAyB,CAAC;IACzC,YAAY,EAAE;QACV,SAAS,EAAE,SAAS,EAAE,CAAC;QACvB,aAAa,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,SAAU,QAAQ,UAAU,MAAM,KAAG,IAAI,IAAI,iBAuG/E,CAAC;AAEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM/G"}
1
+ {"version":3,"file":"rtc.d.ts","sourceRoot":"","sources":["../../../src/matrixrtc/membershipData/rtc.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,KAAK,yBAAyB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7E;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;KACd,CAAC;IACF,WAAW,EAAE,yBAAyB,CAAC;IACvC,UAAU,EAAE;QACR,SAAS,EAAE,SAAS,EAAE,CAAC;QACvB,aAAa,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,SAAU,QAAQ,UAAU,MAAM,KAAG,IAAI,IAAI,iBA8F/E,CAAC;AAEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM7F"}
@@ -109,23 +109,14 @@ export const checkRtcMembershipData = (data, sender) => {
109
109
  if (data.sticky_key !== undefined && data.msc4354_sticky_key !== undefined && data.sticky_key !== data.msc4354_sticky_key) {
110
110
  errors.push(prefix + "sticky_key and msc4354_sticky_key must be equal if both are defined");
111
111
  }
112
- if (data["m.relates_to"] !== undefined) {
113
- const rel = data["m.relates_to"];
114
- if (typeof rel !== "object" || rel === null) {
115
- errors.push(prefix + "m.relates_to must be an object if provided");
116
- } else {
117
- if (typeof rel.event_id !== "string") errors.push(prefix + "m.relates_to.event_id must be a string");
118
- if (rel.rel_type !== "m.reference") errors.push(prefix + "m.relates_to.rel_type must be m.reference");
119
- }
120
- }
121
112
  if (errors.length) {
122
113
  throw new MatrixRTCMembershipParseError("RtcMembership", errors);
123
114
  }
124
115
  return true;
125
116
  };
126
- export async function computeRtcIdentityRaw(userId, deviceId, memberId) {
117
+ export async function computeRtcIdentityRaw(userId, memberId) {
127
118
  // canonical JSON serialization (Matrix canonical JSON for arrays)
128
- const jsonStr = JSON.stringify([userId, deviceId, memberId]);
119
+ const jsonStr = JSON.stringify([userId, memberId]);
129
120
  const hashBuffer = await sha256(jsonStr);
130
121
  const hashedString = encodeUnpaddedBase64(hashBuffer);
131
122
  return hashedString;
@@ -1 +1 @@
1
- {"version":3,"file":"rtc.js","names":[],"sources":["../../../src/matrixrtc/membershipData/rtc.ts"],"sourcesContent":["/*\nCopyright 2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { MXID_PATTERN } from \"../../models/room-member.ts\";\nimport type { IContent } from \"../../models/event.ts\";\nimport type { RelationType } from \"../../types.ts\";\nimport { type RtcSlotApplicationContent, type Transport } from \"../types.ts\";\nimport { MatrixRTCMembershipParseError } from \"./common.ts\";\nimport { sha256 } from \"../../digest.ts\";\nimport { encodeUnpaddedBase64 } from \"../../base64.ts\";\nimport { slotIdToDescription } from \"../utils.ts\";\n\n/**\n * Represents the current form of MSC4143, which uses sticky events to store membership.\n */\nexport interface RtcMembershipData {\n \"slot_id\": string;\n \"member\": {\n user_id: string;\n device_id: string;\n id: string;\n };\n \"m.relates_to\"?: {\n event_id: string;\n rel_type: RelationType.Reference;\n };\n \"application\": RtcSlotApplicationContent;\n \"transports\": {\n published: Transport[];\n can_subscribe: string[];\n };\n \"versions\": string[];\n \"msc4354_sticky_key\"?: string;\n \"sticky_key\"?: string;\n}\n\n/**\n * Validates that `data` matches the format expected by MSC4143.\n * @param data The event content.\n * @param sender The sender of the event.\n * @returns true if `data` is valid RtcMembershipData\n * @throws {MatrixRTCMembershipParseError} if the content is not valid\n */\nexport const checkRtcMembershipData = (data: IContent, sender: string): data is RtcMembershipData => {\n const errors: string[] = [];\n const prefix = \" - \";\n const expectedSlotPrefix = `${data?.application?.type}#`;\n\n // required fields\n if (typeof data.slot_id !== \"string\") {\n errors.push(prefix + \"slot_id must be string\");\n } else if (!data.slot_id.startsWith(expectedSlotPrefix)) {\n errors.push(prefix + `slot_id must start with ${expectedSlotPrefix}`);\n } else {\n try {\n slotIdToDescription(data.slot_id);\n } catch (ex) {\n errors.push(prefix + `slot_id was badly formed${ex instanceof Error ? `: ${ex.message}` : \"\"}`);\n }\n }\n\n if (typeof data.member !== \"object\" || data.member === null) {\n errors.push(prefix + \"member must be an object\");\n } else {\n if (typeof data.member.user_id !== \"string\") {\n errors.push(prefix + \"member.user_id must be string\");\n } else if (!MXID_PATTERN.test(data.member.user_id)) {\n errors.push(prefix + \"member.user_id must be a valid mxid\");\n }\n // This is not what the spec enforces but there currently are no rules what power levels are required to\n // send a m.rtc.member event for a other user. So we add this check for simplicity and to avoid possible attacks until there\n // is a proper definition when this is allowed.\n else if (data.member.user_id !== sender) {\n errors.push(prefix + \"member.user_id must match the sender\");\n }\n if (typeof data.member.device_id !== \"string\") {\n errors.push(prefix + \"member.device_id must be string\");\n }\n if (typeof data.member.id !== \"string\") errors.push(prefix + \"member.id must be string\");\n }\n if (typeof data.application !== \"object\" || data.application === null) {\n errors.push(prefix + \"application must be an object\");\n } else {\n if (typeof data.application.type !== \"string\") {\n errors.push(prefix + \"application.type must be a string\");\n } else {\n if (data.application.type.includes(\"#\")) errors.push(prefix + 'application.type must not include \"#\"');\n }\n }\n if (typeof data.transports !== \"object\" || data.transports === null || !Array.isArray(data.transports.published)) {\n errors.push(prefix + \"transports.published must be an array\");\n } else {\n // validate that each transport has at least a string 'type'\n for (const t of data.transports.published) {\n if (typeof t !== \"object\" || t === null || typeof t.type !== \"string\") {\n errors.push(prefix + \"transports.published entries must be objects with a string type\");\n break;\n }\n }\n }\n if (\n typeof data.transports !== \"object\" ||\n data.transports === null ||\n !Array.isArray(data.transports.can_subscribe) ||\n !data.transports.can_subscribe.every((t: unknown) => typeof t === \"string\")\n ) {\n errors.push(prefix + \"transports.can_subscribe must be an array of strings\");\n }\n if (data.versions === undefined || !Array.isArray(data.versions)) {\n errors.push(prefix + \"versions must be an array\");\n } else if (!data.versions.every((v) => typeof v === \"string\")) {\n errors.push(prefix + \"versions must be an array of strings\");\n }\n\n // optional fields\n if ((data.sticky_key ?? data.msc4354_sticky_key) === undefined) {\n errors.push(prefix + \"sticky_key or msc4354_sticky_key must be a defined\");\n }\n if (data.sticky_key !== undefined && typeof data.sticky_key !== \"string\") {\n errors.push(prefix + \"sticky_key must be a string\");\n }\n if (data.msc4354_sticky_key !== undefined && typeof data.msc4354_sticky_key !== \"string\") {\n errors.push(prefix + \"msc4354_sticky_key must be a string\");\n }\n if (\n data.sticky_key !== undefined &&\n data.msc4354_sticky_key !== undefined &&\n data.sticky_key !== data.msc4354_sticky_key\n ) {\n errors.push(prefix + \"sticky_key and msc4354_sticky_key must be equal if both are defined\");\n }\n if (data[\"m.relates_to\"] !== undefined) {\n const rel = data[\"m.relates_to\"] as RtcMembershipData[\"m.relates_to\"];\n if (typeof rel !== \"object\" || rel === null) {\n errors.push(prefix + \"m.relates_to must be an object if provided\");\n } else {\n if (typeof rel.event_id !== \"string\") errors.push(prefix + \"m.relates_to.event_id must be a string\");\n if (rel.rel_type !== \"m.reference\") errors.push(prefix + \"m.relates_to.rel_type must be m.reference\");\n }\n }\n\n if (errors.length) {\n throw new MatrixRTCMembershipParseError(\"RtcMembership\", errors);\n }\n\n return true;\n};\n\nexport async function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> {\n // canonical JSON serialization (Matrix canonical JSON for arrays)\n const jsonStr = JSON.stringify([userId, deviceId, memberId]);\n const hashBuffer = await sha256(jsonStr);\n const hashedString = encodeUnpaddedBase64(hashBuffer);\n return hashedString;\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAAS,YAAY,QAAQ,6BAA6B;AAI1D,SAAS,6BAA6B,QAAQ,aAAa;AAC3D,SAAS,MAAM,QAAQ,iBAAiB;AACxC,SAAS,oBAAoB,QAAQ,iBAAiB;AACtD,SAAS,mBAAmB,QAAQ,aAAa;;AAEjD;AACA;AACA;;AAsBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,sBAAsB,GAAG,CAAC,IAAc,EAAE,MAAc,KAAgC;EACjG,MAAM,MAAgB,GAAG,EAAE;EAC3B,MAAM,MAAM,GAAG,KAAK;EACpB,MAAM,kBAAkB,GAAG,GAAG,IAAI,EAAE,WAAW,EAAE,IAAI,GAAG;;EAExD;EACA,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE;IAClC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,wBAAwB,CAAC;EAClD,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE;IACrD,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,2BAA2B,kBAAkB,EAAE,CAAC;EACzE,CAAC,MAAM;IACH,IAAI;MACA,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC;IACrC,CAAC,CAAC,OAAO,EAAE,EAAE;MACT,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,2BAA2B,EAAE,YAAY,KAAK,GAAG,KAAK,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;IACnG;EACJ;EAEA,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE;IACzD,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,0BAA0B,CAAC;EACpD,CAAC,MAAM;IACH,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE;MACzC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,+BAA+B,CAAC;IACzD,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;MAChD,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,qCAAqC,CAAC;IAC/D;IACA;IACA;IACA;IAAA,KACK,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,MAAM,EAAE;MACrC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,sCAAsC,CAAC;IAChE;IACA,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,KAAK,QAAQ,EAAE;MAC3C,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,iCAAiC,CAAC;IAC3D;IACA,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,0BAA0B,CAAC;EAC5F;EACA,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE;IACnE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,+BAA+B,CAAC;EACzD,CAAC,MAAM;IACH,IAAI,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,QAAQ,EAAE;MAC3C,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,mCAAmC,CAAC;IAC7D,CAAC,MAAM;MACH,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,uCAAuC,CAAC;IAC1G;EACJ;EACA,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE;IAC9G,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,uCAAuC,CAAC;EACjE,CAAC,MAAM;IACH;IACA,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE;MACvC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE;QACnE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,iEAAiE,CAAC;QACvF;MACJ;IACJ;EACJ;EACA,IACI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IACnC,IAAI,CAAC,UAAU,KAAK,IAAI,IACxB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,IAC7C,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,KAAK,CAAE,CAAU,IAAK,OAAO,CAAC,KAAK,QAAQ,CAAC,EAC7E;IACE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,sDAAsD,CAAC;EAChF;EACA,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;IAC9D,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,2BAA2B,CAAC;EACrD,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAE,CAAC,IAAK,OAAO,CAAC,KAAK,QAAQ,CAAC,EAAE;IAC3D,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,sCAAsC,CAAC;EAChE;;EAEA;EACA,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,kBAAkB,MAAM,SAAS,EAAE;IAC5D,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,oDAAoD,CAAC;EAC9E;EACA,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,EAAE;IACtE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,6BAA6B,CAAC;EACvD;EACA,IAAI,IAAI,CAAC,kBAAkB,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,kBAAkB,KAAK,QAAQ,EAAE;IACtF,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,qCAAqC,CAAC;EAC/D;EACA,IACI,IAAI,CAAC,UAAU,KAAK,SAAS,IAC7B,IAAI,CAAC,kBAAkB,KAAK,SAAS,IACrC,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,kBAAkB,EAC7C;IACE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,qEAAqE,CAAC;EAC/F;EACA,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,SAAS,EAAE;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAsC;IACrE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE;MACzC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,4CAA4C,CAAC;IACtE,CAAC,MAAM;MACH,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,wCAAwC,CAAC;MACpG,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,2CAA2C,CAAC;IACzG;EACJ;EAEA,IAAI,MAAM,CAAC,MAAM,EAAE;IACf,MAAM,IAAI,6BAA6B,CAAC,eAAe,EAAE,MAAM,CAAC;EACpE;EAEA,OAAO,IAAI;AACf,CAAC;AAED,OAAO,eAAe,qBAAqB,CAAC,MAAc,EAAE,QAAgB,EAAE,QAAgB,EAAmB;EAC7G;EACA,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;EAC5D,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;EACxC,MAAM,YAAY,GAAG,oBAAoB,CAAC,UAAU,CAAC;EACrD,OAAO,YAAY;AACvB","ignoreList":[]}
1
+ {"version":3,"file":"rtc.js","names":[],"sources":["../../../src/matrixrtc/membershipData/rtc.ts"],"sourcesContent":["/*\nCopyright 2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { MXID_PATTERN } from \"../../models/room-member.ts\";\nimport type { IContent } from \"../../models/event.ts\";\nimport { type RtcSlotApplicationContent, type Transport } from \"../types.ts\";\nimport { MatrixRTCMembershipParseError } from \"./common.ts\";\nimport { sha256 } from \"../../digest.ts\";\nimport { encodeUnpaddedBase64 } from \"../../base64.ts\";\nimport { slotIdToDescription } from \"../utils.ts\";\n\n/**\n * Represents the current form of MSC4143, which uses sticky events to store membership.\n */\nexport interface RtcMembershipData {\n slot_id: string;\n member: {\n user_id: string;\n device_id: string;\n id: string;\n };\n application: RtcSlotApplicationContent;\n transports: {\n published: Transport[];\n can_subscribe: string[];\n };\n versions: string[];\n msc4354_sticky_key?: string;\n sticky_key?: string;\n}\n\n/**\n * Validates that `data` matches the format expected by MSC4143.\n * @param data The event content.\n * @param sender The sender of the event.\n * @returns true if `data` is valid RtcMembershipData\n * @throws {MatrixRTCMembershipParseError} if the content is not valid\n */\nexport const checkRtcMembershipData = (data: IContent, sender: string): data is RtcMembershipData => {\n const errors: string[] = [];\n const prefix = \" - \";\n const expectedSlotPrefix = `${data?.application?.type}#`;\n\n // required fields\n if (typeof data.slot_id !== \"string\") {\n errors.push(prefix + \"slot_id must be string\");\n } else if (!data.slot_id.startsWith(expectedSlotPrefix)) {\n errors.push(prefix + `slot_id must start with ${expectedSlotPrefix}`);\n } else {\n try {\n slotIdToDescription(data.slot_id);\n } catch (ex) {\n errors.push(prefix + `slot_id was badly formed${ex instanceof Error ? `: ${ex.message}` : \"\"}`);\n }\n }\n\n if (typeof data.member !== \"object\" || data.member === null) {\n errors.push(prefix + \"member must be an object\");\n } else {\n if (typeof data.member.user_id !== \"string\") {\n errors.push(prefix + \"member.user_id must be string\");\n } else if (!MXID_PATTERN.test(data.member.user_id)) {\n errors.push(prefix + \"member.user_id must be a valid mxid\");\n }\n // This is not what the spec enforces but there currently are no rules what power levels are required to\n // send a m.rtc.member event for a other user. So we add this check for simplicity and to avoid possible attacks until there\n // is a proper definition when this is allowed.\n else if (data.member.user_id !== sender) {\n errors.push(prefix + \"member.user_id must match the sender\");\n }\n if (typeof data.member.device_id !== \"string\") {\n errors.push(prefix + \"member.device_id must be string\");\n }\n if (typeof data.member.id !== \"string\") errors.push(prefix + \"member.id must be string\");\n }\n if (typeof data.application !== \"object\" || data.application === null) {\n errors.push(prefix + \"application must be an object\");\n } else {\n if (typeof data.application.type !== \"string\") {\n errors.push(prefix + \"application.type must be a string\");\n } else {\n if (data.application.type.includes(\"#\")) errors.push(prefix + 'application.type must not include \"#\"');\n }\n }\n if (typeof data.transports !== \"object\" || data.transports === null || !Array.isArray(data.transports.published)) {\n errors.push(prefix + \"transports.published must be an array\");\n } else {\n // validate that each transport has at least a string 'type'\n for (const t of data.transports.published) {\n if (typeof t !== \"object\" || t === null || typeof t.type !== \"string\") {\n errors.push(prefix + \"transports.published entries must be objects with a string type\");\n break;\n }\n }\n }\n if (\n typeof data.transports !== \"object\" ||\n data.transports === null ||\n !Array.isArray(data.transports.can_subscribe) ||\n !data.transports.can_subscribe.every((t: unknown) => typeof t === \"string\")\n ) {\n errors.push(prefix + \"transports.can_subscribe must be an array of strings\");\n }\n if (data.versions === undefined || !Array.isArray(data.versions)) {\n errors.push(prefix + \"versions must be an array\");\n } else if (!data.versions.every((v) => typeof v === \"string\")) {\n errors.push(prefix + \"versions must be an array of strings\");\n }\n\n // optional fields\n if ((data.sticky_key ?? data.msc4354_sticky_key) === undefined) {\n errors.push(prefix + \"sticky_key or msc4354_sticky_key must be a defined\");\n }\n if (data.sticky_key !== undefined && typeof data.sticky_key !== \"string\") {\n errors.push(prefix + \"sticky_key must be a string\");\n }\n if (data.msc4354_sticky_key !== undefined && typeof data.msc4354_sticky_key !== \"string\") {\n errors.push(prefix + \"msc4354_sticky_key must be a string\");\n }\n if (\n data.sticky_key !== undefined &&\n data.msc4354_sticky_key !== undefined &&\n data.sticky_key !== data.msc4354_sticky_key\n ) {\n errors.push(prefix + \"sticky_key and msc4354_sticky_key must be equal if both are defined\");\n }\n\n if (errors.length) {\n throw new MatrixRTCMembershipParseError(\"RtcMembership\", errors);\n }\n\n return true;\n};\n\nexport async function computeRtcIdentityRaw(userId: string, memberId: string): Promise<string> {\n // canonical JSON serialization (Matrix canonical JSON for arrays)\n const jsonStr = JSON.stringify([userId, memberId]);\n const hashBuffer = await sha256(jsonStr);\n const hashedString = encodeUnpaddedBase64(hashBuffer);\n return hashedString;\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,5 +1,5 @@
1
1
  import { type IContent } from "../../matrix.ts";
2
- import { type RTCCallIntent, type Transport } from "../types.ts";
2
+ import { type RTCCallCapability, type RTCCallIntent, type Transport } from "../types.ts";
3
3
  /**
4
4
  * (MatrixRTC) session membership data.
5
5
  * This represents the *OLD* form of MSC4143, which uses state events to store membership.
@@ -27,7 +27,7 @@ export type SessionMembershipData = {
27
27
  */
28
28
  "focus_active": {
29
29
  type: "livekit" | string;
30
- focus_selection: "oldest_membership" | "multi_sfu" | string;
30
+ focus_selection: "multi_sfu" | string;
31
31
  };
32
32
  /**
33
33
  * A list of possible foci this user knows about. One of them might be used based on the focus_active
@@ -58,6 +58,10 @@ export type SessionMembershipData = {
58
58
  * something else.
59
59
  */
60
60
  "m.call.intent"?: RTCCallIntent;
61
+ /**
62
+ * The kinds of media this member's device is capable of handling, e.g. audio and/or video.
63
+ */
64
+ "capabilities"?: RTCCallCapability[];
61
65
  /**
62
66
  * The id used on the media backend.
63
67
  * (With livekit this is the participant identity on the LK SFU)
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/matrixrtc/membershipData/session.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAGjE;;;;IAII;AACJ,MAAM,MAAM,qBAAqB,GAAG;IAChC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,cAAc,EAAE;QACZ,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;QACzB,eAAe,EAAE,mBAAmB,GAAG,WAAW,GAAG,MAAM,CAAC;KAC/D,CAAC;IAEF;;;OAGG;IACH,gBAAgB,EAAE,SAAS,EAAE,CAAC;IAE9B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAItB;;;OAGG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAE9B;;;;QAII;IACJ,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC;IAEhC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,SAAU,QAAQ,KAAG,IAAI,IAAI,qBAwCpE,CAAC"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/matrixrtc/membershipData/session.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAGzF;;;;IAII;AACJ,MAAM,MAAM,qBAAqB,GAAG;IAChC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,cAAc,EAAE;QACZ,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;QACzB,eAAe,EAAE,WAAW,GAAG,MAAM,CAAC;KACzC,CAAC;IAEF;;;OAGG;IACH,gBAAgB,EAAE,SAAS,EAAE,CAAC;IAE9B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAItB;;;OAGG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAE9B;;;;QAII;IACJ,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC;IAEhC;;OAEG;IACH,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAErC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,SAAU,QAAQ,KAAG,IAAI,IAAI,qBA+CpE,CAAC"}
@@ -53,6 +53,9 @@ export const checkSessionsMembershipData = data => {
53
53
  if (data["m.call.intent"] !== undefined && typeof data["m.call.intent"] !== "string") {
54
54
  errors.push(prefix + "m.call.intent must be a string");
55
55
  }
56
+ if (data.capabilities !== undefined && !(Array.isArray(data.capabilities) && data.capabilities.every(c => typeof c === "string"))) {
57
+ errors.push(prefix + "capabilities must be an array of strings");
58
+ }
56
59
  if (errors.length) {
57
60
  throw new MatrixRTCMembershipParseError("SessionMembership", errors);
58
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","names":[],"sources":["../../../src/matrixrtc/membershipData/session.ts"],"sourcesContent":["/*\nCopyright 2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { type IContent } from \"../../matrix.ts\";\nimport { type RTCCallIntent, type Transport } from \"../types.ts\";\nimport { MatrixRTCMembershipParseError } from \"./common.ts\";\n\n/**\n * (MatrixRTC) session membership data.\n * This represents the *OLD* form of MSC4143, which uses state events to store membership.\n * Represents the `session` in the memberships section of an m.call.member event as it is on the wire.\n **/\nexport type SessionMembershipData = {\n /**\n * The RTC application defines the type of the RTC session.\n */\n \"application\": string;\n\n /**\n * The id of this session.\n * A session can never span over multiple rooms so this id is to distinguish between\n * multiple session in one room. A room wide session that is not associated with a user,\n * and therefore immune to creation race conflicts, uses the `call_id: \"\"`.\n */\n \"call_id\": string;\n\n /**\n * The Matrix device ID of this session. A single user can have multiple sessions on different devices.\n */\n \"device_id\": string;\n\n /**\n * The focus selection system this user/membership is using.\n * NOTE: This is still included for legacy reasons, but not consumed by the SDK.\n */\n \"focus_active\": {\n type: \"livekit\" | string;\n focus_selection: \"oldest_membership\" | \"multi_sfu\" | string;\n };\n\n /**\n * A list of possible foci this user knows about. One of them might be used based on the focus_active\n * selection system.\n */\n \"foci_preferred\": Transport[];\n\n /**\n * Optional field that contains the creation of the session. If it is undefined the creation\n * is the `origin_server_ts` of the event itself. For updates to the event this property tracks\n * the `origin_server_ts` of the initial join event.\n * - If it is undefined it can be interpreted as a \"Join\".\n * - If it is defined it can be interpreted as an \"Update\"\n */\n \"created_ts\"?: number;\n\n // Application specific data\n\n /**\n * If the `application` = `\"m.call\"` this defines if it is a room or user owned call.\n * There can always be one room scoped call but multiple user owned calls (breakout sessions)\n */\n \"scope\"?: \"m.room\" | \"m.user\";\n\n /**\n * Optionally we allow to define a delta to the `created_ts` that defines when the event is expired/invalid.\n * This should be set to multiple hours. The only reason it exist is to deal with failed delayed events.\n * (for example caused by a homeserver crashes)\n **/\n \"expires\"?: number;\n\n /**\n * The intent of the call from the perspective of this user. This may be an audio call, video call or\n * something else.\n */\n \"m.call.intent\"?: RTCCallIntent;\n\n /**\n * The id used on the media backend.\n * (With livekit this is the participant identity on the LK SFU)\n * This can be a UUID but right now it is `${this.matrixEventData.sender}:${data.device_id}`.\n *\n * It is compleatly valid to not set this field. Other clients will treat `undefined` as `${this.matrixEventData.sender}:${data.device_id}`\n */\n \"membershipID\"?: string;\n};\n\n/**\n * Validates that `data` matches the format expected by the legacy form of MSC4143.\n * @param data The event content.\n * @returns true if `data` is valid SessionMembershipData\n * @throws {MatrixRTCMembershipParseError} if the content is not valid\n */\nexport const checkSessionsMembershipData = (data: IContent): data is SessionMembershipData => {\n const prefix = \" - \";\n const errors: string[] = [];\n if (typeof data.device_id !== \"string\") errors.push(prefix + \"device_id must be string\");\n if (typeof data.call_id !== \"string\") errors.push(prefix + \"call_id must be string\");\n if (typeof data.application !== \"string\") errors.push(prefix + \"application must be a string\");\n if (data.focus_active === undefined) {\n errors.push(prefix + \"focus_active has an invalid type\");\n }\n if (typeof data.focus_active?.type !== \"string\") {\n errors.push(prefix + \"focus_active.type must be a string\");\n }\n if (\n data.foci_preferred !== undefined &&\n !(\n Array.isArray(data.foci_preferred) &&\n data.foci_preferred.every(\n (f: Transport) => typeof f === \"object\" && f !== null && typeof f.type === \"string\",\n )\n )\n ) {\n errors.push(prefix + \"foci_preferred must be an array of transport objects\");\n }\n // optional parameters\n if (data.created_ts !== undefined && typeof data.created_ts !== \"number\") {\n errors.push(prefix + \"created_ts must be number\");\n }\n\n // application specific data (we first need to check if they exist)\n if (data.scope !== undefined && typeof data.scope !== \"string\") errors.push(prefix + \"scope must be string\");\n\n if (data[\"m.call.intent\"] !== undefined && typeof data[\"m.call.intent\"] !== \"string\") {\n errors.push(prefix + \"m.call.intent must be a string\");\n }\n\n if (errors.length) {\n throw new MatrixRTCMembershipParseError(\"SessionMembership\", errors);\n }\n\n return true;\n};\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA,SAAS,6BAA6B,QAAQ,aAAa;;AAE3D;AACA;AACA;AACA;AACA;;AA2EA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,2BAA2B,GAAI,IAAc,IAAoC;EAC1F,MAAM,MAAM,GAAG,KAAK;EACpB,MAAM,MAAgB,GAAG,EAAE;EAC3B,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,0BAA0B,CAAC;EACxF,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,wBAAwB,CAAC;EACpF,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,8BAA8B,CAAC;EAC9F,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE;IACjC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,kCAAkC,CAAC;EAC5D;EACA,IAAI,OAAO,IAAI,CAAC,YAAY,EAAE,IAAI,KAAK,QAAQ,EAAE;IAC7C,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,oCAAoC,CAAC;EAC9D;EACA,IACI,IAAI,CAAC,cAAc,KAAK,SAAS,IACjC,EACI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,IAClC,IAAI,CAAC,cAAc,CAAC,KAAK,CACpB,CAAY,IAAK,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAC/E,CAAC,CACJ,EACH;IACE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,sDAAsD,CAAC;EAChF;EACA;EACA,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,EAAE;IACtE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,2BAA2B,CAAC;EACrD;;EAEA;EACA,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,sBAAsB,CAAC;EAE5G,IAAI,IAAI,CAAC,eAAe,CAAC,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,QAAQ,EAAE;IAClF,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,gCAAgC,CAAC;EAC1D;EAEA,IAAI,MAAM,CAAC,MAAM,EAAE;IACf,MAAM,IAAI,6BAA6B,CAAC,mBAAmB,EAAE,MAAM,CAAC;EACxE;EAEA,OAAO,IAAI;AACf,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"session.js","names":[],"sources":["../../../src/matrixrtc/membershipData/session.ts"],"sourcesContent":["/*\nCopyright 2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { type IContent } from \"../../matrix.ts\";\nimport { type RTCCallCapability, type RTCCallIntent, type Transport } from \"../types.ts\";\nimport { MatrixRTCMembershipParseError } from \"./common.ts\";\n\n/**\n * (MatrixRTC) session membership data.\n * This represents the *OLD* form of MSC4143, which uses state events to store membership.\n * Represents the `session` in the memberships section of an m.call.member event as it is on the wire.\n **/\nexport type SessionMembershipData = {\n /**\n * The RTC application defines the type of the RTC session.\n */\n \"application\": string;\n\n /**\n * The id of this session.\n * A session can never span over multiple rooms so this id is to distinguish between\n * multiple session in one room. A room wide session that is not associated with a user,\n * and therefore immune to creation race conflicts, uses the `call_id: \"\"`.\n */\n \"call_id\": string;\n\n /**\n * The Matrix device ID of this session. A single user can have multiple sessions on different devices.\n */\n \"device_id\": string;\n\n /**\n * The focus selection system this user/membership is using.\n * NOTE: This is still included for legacy reasons, but not consumed by the SDK.\n */\n \"focus_active\": {\n type: \"livekit\" | string;\n focus_selection: \"multi_sfu\" | string;\n };\n\n /**\n * A list of possible foci this user knows about. One of them might be used based on the focus_active\n * selection system.\n */\n \"foci_preferred\": Transport[];\n\n /**\n * Optional field that contains the creation of the session. If it is undefined the creation\n * is the `origin_server_ts` of the event itself. For updates to the event this property tracks\n * the `origin_server_ts` of the initial join event.\n * - If it is undefined it can be interpreted as a \"Join\".\n * - If it is defined it can be interpreted as an \"Update\"\n */\n \"created_ts\"?: number;\n\n // Application specific data\n\n /**\n * If the `application` = `\"m.call\"` this defines if it is a room or user owned call.\n * There can always be one room scoped call but multiple user owned calls (breakout sessions)\n */\n \"scope\"?: \"m.room\" | \"m.user\";\n\n /**\n * Optionally we allow to define a delta to the `created_ts` that defines when the event is expired/invalid.\n * This should be set to multiple hours. The only reason it exist is to deal with failed delayed events.\n * (for example caused by a homeserver crashes)\n **/\n \"expires\"?: number;\n\n /**\n * The intent of the call from the perspective of this user. This may be an audio call, video call or\n * something else.\n */\n \"m.call.intent\"?: RTCCallIntent;\n\n /**\n * The kinds of media this member's device is capable of handling, e.g. audio and/or video.\n */\n \"capabilities\"?: RTCCallCapability[];\n\n /**\n * The id used on the media backend.\n * (With livekit this is the participant identity on the LK SFU)\n * This can be a UUID but right now it is `${this.matrixEventData.sender}:${data.device_id}`.\n *\n * It is compleatly valid to not set this field. Other clients will treat `undefined` as `${this.matrixEventData.sender}:${data.device_id}`\n */\n \"membershipID\"?: string;\n};\n\n/**\n * Validates that `data` matches the format expected by the legacy form of MSC4143.\n * @param data The event content.\n * @returns true if `data` is valid SessionMembershipData\n * @throws {MatrixRTCMembershipParseError} if the content is not valid\n */\nexport const checkSessionsMembershipData = (data: IContent): data is SessionMembershipData => {\n const prefix = \" - \";\n const errors: string[] = [];\n if (typeof data.device_id !== \"string\") errors.push(prefix + \"device_id must be string\");\n if (typeof data.call_id !== \"string\") errors.push(prefix + \"call_id must be string\");\n if (typeof data.application !== \"string\") errors.push(prefix + \"application must be a string\");\n if (data.focus_active === undefined) {\n errors.push(prefix + \"focus_active has an invalid type\");\n }\n if (typeof data.focus_active?.type !== \"string\") {\n errors.push(prefix + \"focus_active.type must be a string\");\n }\n if (\n data.foci_preferred !== undefined &&\n !(\n Array.isArray(data.foci_preferred) &&\n data.foci_preferred.every(\n (f: Transport) => typeof f === \"object\" && f !== null && typeof f.type === \"string\",\n )\n )\n ) {\n errors.push(prefix + \"foci_preferred must be an array of transport objects\");\n }\n // optional parameters\n if (data.created_ts !== undefined && typeof data.created_ts !== \"number\") {\n errors.push(prefix + \"created_ts must be number\");\n }\n\n // application specific data (we first need to check if they exist)\n if (data.scope !== undefined && typeof data.scope !== \"string\") errors.push(prefix + \"scope must be string\");\n\n if (data[\"m.call.intent\"] !== undefined && typeof data[\"m.call.intent\"] !== \"string\") {\n errors.push(prefix + \"m.call.intent must be a string\");\n }\n\n if (\n data.capabilities !== undefined &&\n !(Array.isArray(data.capabilities) && data.capabilities.every((c: unknown) => typeof c === \"string\"))\n ) {\n errors.push(prefix + \"capabilities must be an array of strings\");\n }\n\n if (errors.length) {\n throw new MatrixRTCMembershipParseError(\"SessionMembership\", errors);\n }\n\n return true;\n};\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA,SAAS,6BAA6B,QAAQ,aAAa;;AAE3D;AACA;AACA;AACA;AACA;;AAgFA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,2BAA2B,GAAI,IAAc,IAAoC;EAC1F,MAAM,MAAM,GAAG,KAAK;EACpB,MAAM,MAAgB,GAAG,EAAE;EAC3B,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,0BAA0B,CAAC;EACxF,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,wBAAwB,CAAC;EACpF,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,8BAA8B,CAAC;EAC9F,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE;IACjC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,kCAAkC,CAAC;EAC5D;EACA,IAAI,OAAO,IAAI,CAAC,YAAY,EAAE,IAAI,KAAK,QAAQ,EAAE;IAC7C,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,oCAAoC,CAAC;EAC9D;EACA,IACI,IAAI,CAAC,cAAc,KAAK,SAAS,IACjC,EACI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,IAClC,IAAI,CAAC,cAAc,CAAC,KAAK,CACpB,CAAY,IAAK,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAC/E,CAAC,CACJ,EACH;IACE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,sDAAsD,CAAC;EAChF;EACA;EACA,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,EAAE;IACtE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,2BAA2B,CAAC;EACrD;;EAEA;EACA,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,sBAAsB,CAAC;EAE5G,IAAI,IAAI,CAAC,eAAe,CAAC,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,QAAQ,EAAE;IAClF,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,gCAAgC,CAAC;EAC1D;EAEA,IACI,IAAI,CAAC,YAAY,KAAK,SAAS,IAC/B,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAE,CAAU,IAAK,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,EACvG;IACE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,0CAA0C,CAAC;EACpE;EAEA,IAAI,MAAM,CAAC,MAAM,EAAE;IACf,MAAM,IAAI,6BAA6B,CAAC,mBAAmB,EAAE,MAAM,CAAC;EACxE;EAEA,OAAO,IAAI;AACf,CAAC","ignoreList":[]}
@@ -80,6 +80,11 @@ export type RTCNotificationType = "ring" | "notification";
80
80
  * May be any string, although `"audio"` and `"video"` are commonly accepted values.
81
81
  */
82
82
  export type RTCCallIntent = "audio" | "video" | string;
83
+ /**
84
+ * Represents one kind of media a member's device is capable of handling.
85
+ * May be any string, although `"m.render_audio"` and `"m.render_video"` are commonly accepted values.
86
+ */
87
+ export type RTCCallCapability = "m.render_audio" | "m.render_video" | string;
83
88
  /**
84
89
  * The maximum `lifetime` of an RTC notification as per MSC4075. Larger values are capped to this.
85
90
  *