matrix-js-sdk 42.3.0 → 42.4.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 (96) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/lib/@types/json.d.ts.map +1 -1
  3. package/lib/@types/json.js.map +1 -1
  4. package/lib/client.d.ts +74 -5
  5. package/lib/client.d.ts.map +1 -1
  6. package/lib/client.js +83 -7
  7. package/lib/client.js.map +1 -1
  8. package/lib/common-crypto/CryptoBackend.d.ts +42 -23
  9. package/lib/common-crypto/CryptoBackend.d.ts.map +1 -1
  10. package/lib/common-crypto/CryptoBackend.js +7 -0
  11. package/lib/common-crypto/CryptoBackend.js.map +1 -1
  12. package/lib/embedded.d.ts +35 -1
  13. package/lib/embedded.d.ts.map +1 -1
  14. package/lib/embedded.js +49 -1
  15. package/lib/embedded.js.map +1 -1
  16. package/lib/http-api/fetch.d.ts +0 -1
  17. package/lib/http-api/fetch.d.ts.map +1 -1
  18. package/lib/http-api/fetch.js +3 -22
  19. package/lib/http-api/fetch.js.map +1 -1
  20. package/lib/http-api/logging.d.ts +10 -0
  21. package/lib/http-api/logging.d.ts.map +1 -0
  22. package/lib/http-api/logging.js +46 -0
  23. package/lib/http-api/logging.js.map +1 -0
  24. package/lib/matrixrtc/EncryptionManager.d.ts +5 -0
  25. package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
  26. package/lib/matrixrtc/EncryptionManager.js.map +1 -1
  27. package/lib/matrixrtc/LivekitTransport.d.ts +82 -0
  28. package/lib/matrixrtc/LivekitTransport.d.ts.map +1 -1
  29. package/lib/matrixrtc/LivekitTransport.js +25 -0
  30. package/lib/matrixrtc/LivekitTransport.js.map +1 -1
  31. package/lib/matrixrtc/MatrixRTCSession.d.ts +41 -2
  32. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
  33. package/lib/matrixrtc/MatrixRTCSession.js +59 -1
  34. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  35. package/lib/matrixrtc/MembershipManager.js +7 -4
  36. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  37. package/lib/matrixrtc/RTCEncryptionManager.d.ts +17 -0
  38. package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
  39. package/lib/matrixrtc/RTCEncryptionManager.js +68 -29
  40. package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
  41. package/lib/models/room-receipts.d.ts +9 -3
  42. package/lib/models/room-receipts.d.ts.map +1 -1
  43. package/lib/models/room-receipts.js +51 -11
  44. package/lib/models/room-receipts.js.map +1 -1
  45. package/lib/oauth/authorize.d.ts +7 -2
  46. package/lib/oauth/authorize.d.ts.map +1 -1
  47. package/lib/oauth/authorize.js +10 -4
  48. package/lib/oauth/authorize.js.map +1 -1
  49. package/lib/oauth/fetch.d.ts +17 -0
  50. package/lib/oauth/fetch.d.ts.map +1 -0
  51. package/lib/oauth/fetch.js +47 -0
  52. package/lib/oauth/fetch.js.map +1 -0
  53. package/lib/oauth/index.d.ts +15 -2
  54. package/lib/oauth/index.d.ts.map +1 -1
  55. package/lib/oauth/index.js +33 -12
  56. package/lib/oauth/index.js.map +1 -1
  57. package/lib/rust-crypto/index.d.ts +17 -0
  58. package/lib/rust-crypto/index.d.ts.map +1 -1
  59. package/lib/rust-crypto/index.js +4 -2
  60. package/lib/rust-crypto/index.js.map +1 -1
  61. package/lib/rust-crypto/rust-crypto.d.ts +6 -26
  62. package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
  63. package/lib/rust-crypto/rust-crypto.js +19 -54
  64. package/lib/rust-crypto/rust-crypto.js.map +1 -1
  65. package/lib/sliding-sync-sdk.d.ts.map +1 -1
  66. package/lib/sliding-sync-sdk.js +65 -27
  67. package/lib/sliding-sync-sdk.js.map +1 -1
  68. package/lib/sliding-sync.d.ts +17 -0
  69. package/lib/sliding-sync.d.ts.map +1 -1
  70. package/lib/sliding-sync.js +21 -9
  71. package/lib/sliding-sync.js.map +1 -1
  72. package/lib/sync.d.ts +9 -1
  73. package/lib/sync.d.ts.map +1 -1
  74. package/lib/sync.js +54 -29
  75. package/lib/sync.js.map +1 -1
  76. package/package.json +5 -5
  77. package/src/@types/json.ts +11 -2
  78. package/src/client.ts +126 -11
  79. package/src/common-crypto/CryptoBackend.ts +45 -23
  80. package/src/embedded.ts +67 -4
  81. package/src/http-api/fetch.ts +2 -23
  82. package/src/http-api/logging.ts +46 -0
  83. package/src/matrixrtc/EncryptionManager.ts +6 -0
  84. package/src/matrixrtc/LivekitTransport.ts +86 -0
  85. package/src/matrixrtc/MatrixRTCSession.ts +81 -1
  86. package/src/matrixrtc/MembershipManager.ts +7 -7
  87. package/src/matrixrtc/RTCEncryptionManager.ts +80 -31
  88. package/src/models/room-receipts.ts +54 -10
  89. package/src/oauth/authorize.ts +10 -2
  90. package/src/oauth/fetch.ts +54 -0
  91. package/src/oauth/index.ts +29 -10
  92. package/src/rust-crypto/index.ts +23 -0
  93. package/src/rust-crypto/rust-crypto.ts +25 -67
  94. package/src/sliding-sync-sdk.ts +73 -33
  95. package/src/sliding-sync.ts +31 -14
  96. package/src/sync.ts +58 -35
@@ -103,6 +103,25 @@ export interface InitRustCryptoArgs {
103
103
  * considered verified, without any manual verification taking place.
104
104
  */
105
105
  caCertsPem?: string;
106
+
107
+ /**
108
+ * Optional async function for signing some data with an X.509 certificate.
109
+ * Used to sign the user's identity so compatible clients will recognise
110
+ * this user as verified without manual verification taking place. If you
111
+ * supply this you must also supply {@link InitRustCryptoArgs#x509Validity}.
112
+ */
113
+ x509Signer?: (item: Uint8Array) => Promise<{
114
+ signature_bytes: Uint8Array;
115
+ certificate_chain: string;
116
+ signature_scheme: "RsaPssSha512";
117
+ }>;
118
+
119
+ /**
120
+ * Optional function returning the validity period of the X.509 certificate
121
+ * used for signing, as the number of milliseconds since the Unix epoch. If
122
+ * you supply this you must also supply {@link InitRustCryptoArgs#x509Signer}.
123
+ */
124
+ x509Validity?: () => number;
106
125
  }
107
126
 
108
127
  /**
@@ -158,6 +177,8 @@ async function initOlmMachine(
158
177
  legacyCryptoStore,
159
178
  enableEncryptedStateEvents,
160
179
  caCertsPem,
180
+ x509Signer,
181
+ x509Validity,
161
182
  }: InitRustCryptoArgs,
162
183
  storeHandle: StoreHandle,
163
184
  ): Promise<RustCrypto> {
@@ -169,6 +190,8 @@ async function initOlmMachine(
169
190
  storeHandle,
170
191
  logger,
171
192
  caCertsPem,
193
+ x509Signer,
194
+ x509Validity,
172
195
  );
173
196
 
174
197
  // A final migration step, now that we have an OlmMachine.
@@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
19
19
 
20
20
  import type { IMegolmSessionData } from "../@types/crypto.ts";
21
21
  import { KnownMembership } from "../@types/membership.ts";
22
- import { type IDeviceLists, type IToDeviceEvent, type ReceivedToDeviceMessage } from "../sync-accumulator.ts";
22
+ import { type IToDeviceEvent, type ReceivedToDeviceMessage } from "../sync-accumulator.ts";
23
23
  import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts";
24
24
  import { type MatrixEvent, MatrixEventEvent } from "../models/event.ts";
25
25
  import { type Room } from "../models/room.ts";
@@ -30,6 +30,7 @@ import {
30
30
  DecryptionError,
31
31
  type EventDecryptionResult,
32
32
  type OnSyncCompletedData,
33
+ type SyncCryptoChanges,
33
34
  } from "../common-crypto/CryptoBackend.ts";
34
35
  import { type Logger, LogSpan } from "../logger.ts";
35
36
  import { type IHttpOpts, type MatrixHttpApi, Method } from "../http-api/index.ts";
@@ -292,12 +293,12 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
292
293
  public async decryptEvent(event: MatrixEvent): Promise<EventDecryptionResult> {
293
294
  const roomId = event.getRoomId();
294
295
  if (!roomId) {
295
- // presumably, a to-device message. These are normally decrypted in preprocessToDeviceMessages
296
+ // presumably, a to-device message. These are normally decrypted in processSyncChanges
296
297
  // so the fact it has come back here suggests that decryption failed.
297
298
  //
298
299
  // once we drop support for the libolm crypto implementation, we can stop passing to-device messages
299
300
  // through decryptEvent and hence get rid of this case.
300
- throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
301
+ throw new Error("to-device event was not decrypted in processSyncChanges");
301
302
  }
302
303
  return await this.eventDecryptor.attemptEventDecryption(event, this.deviceIsolationMode);
303
304
  }
@@ -1691,41 +1692,31 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
1691
1692
  ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1692
1693
 
1693
1694
  /**
1694
- * Apply sync changes to the olm machine
1695
- * @param events - the received to-device messages
1696
- * @param oneTimeKeysCounts - the received one time key counts
1697
- * @param unusedFallbackKeys - the received unused fallback keys
1698
- * @param devices - the received device list updates
1699
- * @returns A list of processed to-device messages.
1695
+ * Implementation of {@link SyncCryptoCallbacks.processSyncChanges}.
1696
+ *
1697
+ * Passes all of the encryption-relevant data from a sync response to the OlmMachine in a single call, and
1698
+ * post-processes the resulting to-device messages.
1700
1699
  */
1701
- private async receiveSyncChanges({
1702
- events,
1703
- oneTimeKeysCounts = new Map<string, number>(),
1700
+ public async processSyncChanges({
1701
+ toDeviceEvents,
1702
+ deviceLists,
1703
+ oneTimeKeysCounts,
1704
1704
  unusedFallbackKeys,
1705
- devices = new RustSdkCryptoJs.DeviceLists(),
1706
- }: {
1707
- events?: IToDeviceEvent[];
1708
- oneTimeKeysCounts?: Map<string, number>;
1709
- unusedFallbackKeys?: Set<string>;
1710
- devices?: RustSdkCryptoJs.DeviceLists;
1711
- }): Promise<RustSdkCryptoJs.ProcessedToDeviceEvent[]> {
1712
- return await this.olmMachine.receiveSyncChanges(
1713
- events ? JSON.stringify(events) : "[]",
1714
- devices,
1715
- oneTimeKeysCounts,
1716
- unusedFallbackKeys,
1705
+ useMsc4186 = false,
1706
+ }: SyncCryptoChanges): Promise<ReceivedToDeviceMessage[]> {
1707
+ const events = JSON.stringify(toDeviceEvents);
1708
+ const devices = new RustSdkCryptoJs.DeviceLists(
1709
+ deviceLists?.changed?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
1710
+ deviceLists?.left?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
1717
1711
  );
1718
- }
1712
+ const counts = new Map(Object.entries(oneTimeKeysCounts ?? {}));
1713
+ const fallbackKeys = unusedFallbackKeys && new Set(unusedFallbackKeys);
1719
1714
 
1720
- /** called by the sync loop to preprocess incoming to-device messages
1721
- *
1722
- * @param events - the received to-device messages
1723
- * @returns A list of preprocessed to-device messages.
1724
- */
1725
- public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]> {
1726
- // send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
1727
- // one-time-keys, or fallback keys, so just pass empty data.
1728
- const processed = await this.receiveSyncChanges({ events });
1715
+ // The two variants differ only in how they treat a missing one-time key count: zero keys on the server (sync
1716
+ // v2) versus unchanged (sliding sync).
1717
+ const processed = useMsc4186
1718
+ ? await this.olmMachine.receiveSyncChangesMsc4186(events, devices, counts, fallbackKeys)
1719
+ : await this.olmMachine.receiveSyncChanges(events, devices, counts, fallbackKeys);
1729
1720
 
1730
1721
  const received: ReceivedToDeviceMessage[] = [];
1731
1722
 
@@ -1808,39 +1799,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
1808
1799
  return received;
1809
1800
  }
1810
1801
 
1811
- /** called by the sync loop to process one time key counts and unused fallback keys
1812
- *
1813
- * @param oneTimeKeysCounts - the received one time key counts
1814
- * @param unusedFallbackKeys - the received unused fallback keys
1815
- */
1816
- public async processKeyCounts(
1817
- oneTimeKeysCounts?: Record<string, number>,
1818
- unusedFallbackKeys?: string[],
1819
- ): Promise<void> {
1820
- const mapOneTimeKeysCount = oneTimeKeysCounts && new Map<string, number>(Object.entries(oneTimeKeysCounts));
1821
- const setUnusedFallbackKeys = unusedFallbackKeys && new Set<string>(unusedFallbackKeys);
1822
-
1823
- if (mapOneTimeKeysCount !== undefined || setUnusedFallbackKeys !== undefined) {
1824
- await this.receiveSyncChanges({
1825
- oneTimeKeysCounts: mapOneTimeKeysCount,
1826
- unusedFallbackKeys: setUnusedFallbackKeys,
1827
- });
1828
- }
1829
- }
1830
-
1831
- /** called by the sync loop to process the notification that device lists have
1832
- * been changed.
1833
- *
1834
- * @param deviceLists - device_lists field from /sync
1835
- */
1836
- public async processDeviceLists(deviceLists: IDeviceLists): Promise<void> {
1837
- const devices = new RustSdkCryptoJs.DeviceLists(
1838
- deviceLists.changed?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
1839
- deviceLists.left?.map((userId) => new RustSdkCryptoJs.UserId(userId)),
1840
- );
1841
- await this.receiveSyncChanges({ devices });
1842
- }
1843
-
1844
1802
  /** called by the sync loop on m.room.encryption events
1845
1803
  *
1846
1804
  * @param room - in which the event was received
@@ -28,7 +28,7 @@ import {
28
28
  defaultClientOpts,
29
29
  defaultSyncApiOpts,
30
30
  type SetPresence,
31
- processToDeviceMessages,
31
+ processSyncCryptoChanges,
32
32
  } from "./sync.ts";
33
33
  import { type MatrixEvent } from "./models/event.ts";
34
34
  import {
@@ -39,7 +39,7 @@ import {
39
39
  type IStickyStateEvent,
40
40
  type IStrippedState,
41
41
  type ISyncResponse,
42
- type ReceivedToDeviceMessage,
42
+ type IToDeviceEvent,
43
43
  } from "./sync-accumulator.ts";
44
44
  import { MatrixError } from "./http-api/index.ts";
45
45
  import {
@@ -74,8 +74,63 @@ type ExtensionE2EEResponse = Pick<
74
74
  | "org.matrix.msc2732.device_unused_fallback_key_types"
75
75
  >;
76
76
 
77
+ /**
78
+ * Collects the encryption-relevant parts of a sliding sync response, which arrive via two separate extensions
79
+ * (`e2ee` and `to_device`), so that they can be passed to the crypto layer in a single call once the whole response
80
+ * has been processed. See {@link SyncCryptoCallbacks.processSyncChanges} for why this matters.
81
+ */
82
+ class E2EESyncChangesCollector {
83
+ private toDeviceEvents: IToDeviceEvent[] = [];
84
+ private e2ee?: ExtensionE2EEResponse;
85
+ private hasChanges = false;
86
+
87
+ public constructor(
88
+ private readonly client: MatrixClient,
89
+ private readonly cryptoCallbacks?: SyncCryptoCallbacks,
90
+ ) {}
91
+
92
+ public onToDeviceEvents(events: IToDeviceEvent[]): void {
93
+ this.toDeviceEvents = events;
94
+ this.hasChanges = true;
95
+ }
96
+
97
+ public onE2EEChanges(data: ExtensionE2EEResponse): void {
98
+ this.e2ee = data;
99
+ this.hasChanges = true;
100
+ }
101
+
102
+ /**
103
+ * Pass the collected changes to the crypto layer, and emit the resulting to-device messages on the client.
104
+ *
105
+ * A no-op if nothing has been collected since the last flush, so it is safe to call once per extension.
106
+ */
107
+ public async flush(): Promise<void> {
108
+ if (!this.hasChanges) return;
109
+ const toDeviceEvents = this.toDeviceEvents;
110
+ const e2ee = this.e2ee;
111
+ this.toDeviceEvents = [];
112
+ this.e2ee = undefined;
113
+ this.hasChanges = false;
114
+
115
+ // Fields omitted from the `e2ee` extension are unchanged since the last response; the crypto layer knows to
116
+ // interpret them that way given `useMsc4186`.
117
+ await processSyncCryptoChanges(this.client, this.cryptoCallbacks, {
118
+ toDeviceEvents,
119
+ deviceLists: e2ee?.device_lists,
120
+ oneTimeKeysCounts: e2ee?.device_one_time_keys_count,
121
+ unusedFallbackKeys:
122
+ e2ee?.device_unused_fallback_key_types ?? e2ee?.["org.matrix.msc2732.device_unused_fallback_key_types"],
123
+ useMsc4186: true,
124
+ });
125
+ this.cryptoCallbacks?.onSyncCompleted({});
126
+ }
127
+ }
128
+
77
129
  class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResponse> {
78
- public constructor(private readonly crypto: SyncCryptoCallbacks) {}
130
+ public constructor(
131
+ private readonly crypto: SyncCryptoCallbacks,
132
+ private readonly collector: E2EESyncChangesCollector,
133
+ ) {}
79
134
 
80
135
  public name(): string {
81
136
  return "e2ee";
@@ -102,18 +157,11 @@ class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResp
102
157
  }
103
158
 
104
159
  public async onResponse(data: ExtensionE2EEResponse): Promise<void> {
105
- // Handle device list updates
106
- if (data.device_lists) {
107
- await this.crypto.processDeviceLists(data.device_lists);
108
- }
109
-
110
- // Handle one_time_keys_count and unused_fallback_key_types
111
- await this.crypto.processKeyCounts(
112
- data.device_one_time_keys_count,
113
- data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"],
114
- );
160
+ this.collector.onE2EEChanges(data);
161
+ }
115
162
 
116
- this.crypto.onSyncCompleted({});
163
+ public async onResponseComplete(): Promise<void> {
164
+ await this.collector.flush();
117
165
  }
118
166
  }
119
167
 
@@ -131,10 +179,7 @@ type ExtensionToDeviceResponse = {
131
179
  class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
132
180
  private nextBatch: string | null = null;
133
181
 
134
- public constructor(
135
- private readonly client: MatrixClient,
136
- private readonly cryptoCallbacks?: SyncCryptoCallbacks,
137
- ) {}
182
+ public constructor(private readonly collector: E2EESyncChangesCollector) {}
138
183
 
139
184
  public name(): string {
140
185
  return "to_device";
@@ -153,21 +198,13 @@ class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, Extension
153
198
  }
154
199
 
155
200
  public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
156
- const events = data["events"] || [];
157
- let receivedToDeviceMessages: ReceivedToDeviceMessage[];
158
- if (this.cryptoCallbacks) {
159
- receivedToDeviceMessages = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
160
- } else {
161
- // Crypto is not enabled, so we just return the events.
162
- receivedToDeviceMessages = events.map((rawEvent) => ({
163
- message: rawEvent,
164
- encryptionInfo: null,
165
- }));
166
- }
167
- processToDeviceMessages(receivedToDeviceMessages, this.client);
168
-
201
+ this.collector.onToDeviceEvents(data["events"] || []);
169
202
  this.nextBatch = data.next_batch;
170
203
  }
204
+
205
+ public async onResponseComplete(): Promise<void> {
206
+ await this.collector.flush();
207
+ }
171
208
  }
172
209
 
173
210
  type ExtensionAccountDataRequest = {
@@ -408,15 +445,18 @@ export class SlidingSyncSdk {
408
445
 
409
446
  this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
410
447
  this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
448
+ // The `e2ee` and `to_device` extensions feed a shared collector, so that the crypto layer sees all the
449
+ // encryption-relevant data from a response in a single call.
450
+ const e2eeCollector = new E2EESyncChangesCollector(this.client, this.syncOpts.cryptoCallbacks);
411
451
  const extensions: Extension<any, any>[] = [
412
- new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks),
452
+ new ExtensionToDevice(e2eeCollector),
413
453
  new ExtensionAccountData(this.client),
414
454
  new ExtensionTyping(this.client),
415
455
  new ExtensionReceipts(this.client),
416
456
  new ExtensionStickyEvents(this.client),
417
457
  ];
418
458
  if (this.syncOpts.cryptoCallbacks) {
419
- extensions.push(new ExtensionE2EE(this.syncOpts.cryptoCallbacks));
459
+ extensions.push(new ExtensionE2EE(this.syncOpts.cryptoCallbacks, e2eeCollector));
420
460
  }
421
461
  extensions.forEach((ext) => {
422
462
  this.slidingSync.registerExtension(ext);
@@ -249,6 +249,15 @@ export interface Extension<Req extends object, Res extends object> {
249
249
  * @returns The state when it should be called.
250
250
  */
251
251
  when(): ExtensionState;
252
+ /**
253
+ * An optional function which is called once per sync response, after `onResponse` has been called on all of
254
+ * the extensions (of the same `when()` stage) which were present in the response. It is called even if this
255
+ * extension was not itself present in the response.
256
+ *
257
+ * This allows extensions which need to combine their data with that of other extensions (such as `e2ee` and
258
+ * `to_device`) to act once the whole response has been seen.
259
+ */
260
+ onResponseComplete?(): Promise<void>;
252
261
  }
253
262
 
254
263
  /**
@@ -487,23 +496,31 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
487
496
  }
488
497
 
489
498
  private async onPreExtensionsResponse(ext: Record<string, object>): Promise<void> {
490
- await Promise.all(
491
- Object.keys(ext).map(async (extName) => {
492
- if (this.extensions[extName].when() == ExtensionState.PreProcess) {
493
- await this.extensions[extName].onResponse(ext[extName]);
494
- }
495
- }),
496
- );
499
+ await this.onExtensionsResponse(ext, ExtensionState.PreProcess);
497
500
  }
498
501
 
499
502
  private async onPostExtensionsResponse(ext: Record<string, object>): Promise<void> {
500
- await Promise.all(
501
- Object.keys(ext).map(async (extName) => {
502
- if (this.extensions[extName].when() == ExtensionState.PostProcess) {
503
- await this.extensions[extName].onResponse(ext[extName]);
504
- }
505
- }),
506
- );
503
+ await this.onExtensionsResponse(ext, ExtensionState.PostProcess);
504
+ }
505
+
506
+ /**
507
+ * Call `onResponse` on each registered extension of the given stage which is present in the response, and then
508
+ * `onResponseComplete` on every registered extension of that stage.
509
+ *
510
+ * Extensions are processed sequentially, in registration order, so that extensions whose data must be combined
511
+ * (such as `e2ee` and `to_device`) see a deterministic order.
512
+ */
513
+ private async onExtensionsResponse(ext: Record<string, object>, state: ExtensionState): Promise<void> {
514
+ const extensions = Object.values(this.extensions).filter((extension) => extension.when() === state);
515
+ for (const extension of extensions) {
516
+ const data = ext[extension.name()];
517
+ if (data !== undefined) {
518
+ await extension.onResponse(data);
519
+ }
520
+ }
521
+ for (const extension of extensions) {
522
+ await extension.onResponseComplete?.();
523
+ }
507
524
  }
508
525
 
509
526
  /**
package/src/sync.ts CHANGED
@@ -23,7 +23,7 @@ limitations under the License.
23
23
  * for HTTP and WS at some point.
24
24
  */
25
25
 
26
- import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
26
+ import type { SyncCryptoCallbacks, SyncCryptoChanges } from "./common-crypto/CryptoBackend.ts";
27
27
  import { type SyncUserProfile, User } from "./models/user.ts";
28
28
  import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
29
29
  import { deepCopy, noUnsafeEventProps, unsafeProp } from "./utils.ts";
@@ -1185,24 +1185,29 @@ export class SyncApi {
1185
1185
  }
1186
1186
  }
1187
1187
 
1188
- // handle to-device events
1189
- if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
1190
- const toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps);
1191
-
1192
- let receivedToDeviceMessages: ReceivedToDeviceMessage[];
1193
- if (this.syncOpts.cryptoCallbacks) {
1194
- receivedToDeviceMessages =
1195
- await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
1196
- } else {
1197
- // Crypto is not enabled, so we just return the events.
1198
- receivedToDeviceMessages = toDeviceMessages.map((rawEvent) => ({
1199
- message: rawEvent,
1200
- encryptionInfo: null,
1201
- }));
1202
- }
1188
+ // Handle to-device events, device list changes, one-time key counts and unused fallback keys.
1189
+ //
1190
+ // These are passed to the crypto layer together, in a single call: see the documentation of
1191
+ // `SyncCryptoCallbacks.processSyncChanges` for why they must not be split up. This has to happen before we
1192
+ // process the room events, so that any room keys received in to-device messages can be used to decrypt them.
1193
+ const toDeviceEvents: IToDeviceEvent[] = Array.isArray(data.to_device?.events) ? data.to_device.events : [];
1194
+
1195
+ // A cached sync (see `syncFromCache`) carries no E2EE data at all, so we skip the crypto layer for it: an
1196
+ // absent `device_one_time_keys_count` would otherwise be taken to mean that there are no one-time keys on the
1197
+ // server, triggering a spurious key upload on every restart.
1198
+ if (!syncEventData.fromCache) {
1199
+ await processSyncCryptoChanges(client, this.syncOpts.cryptoCallbacks, {
1200
+ toDeviceEvents,
1201
+ deviceLists: data.device_lists,
1202
+ // Per the spec, an absent `device_one_time_keys_count` means there are no one-time keys on the server.
1203
+ oneTimeKeysCounts: data.device_one_time_keys_count ?? {},
1204
+ unusedFallbackKeys:
1205
+ data.device_unused_fallback_key_types ??
1206
+ data["org.matrix.msc2732.device_unused_fallback_key_types"],
1207
+ });
1208
+ }
1203
1209
 
1204
- processToDeviceMessages(receivedToDeviceMessages, client);
1205
- } else {
1210
+ if (toDeviceEvents.length === 0) {
1206
1211
  // no more to-device events: we can stop polling with a short timeout.
1207
1212
  this.catchingUp = false;
1208
1213
  }
@@ -1567,23 +1572,6 @@ export class SyncApi {
1567
1572
  client.getNotifTimelineSet()?.addLiveEvent(event, { addToState: true });
1568
1573
  });
1569
1574
  }
1570
-
1571
- // Handle device list updates
1572
- if (data.device_lists) {
1573
- if (this.syncOpts.cryptoCallbacks) {
1574
- await this.syncOpts.cryptoCallbacks.processDeviceLists(data.device_lists);
1575
- } else {
1576
- // FIXME if we *don't* have a crypto module, we still need to
1577
- // invalidate the device lists. But that would require a
1578
- // substantial bit of rework :/.
1579
- }
1580
- }
1581
-
1582
- // Handle one_time_keys_count and unused fallback keys
1583
- await this.syncOpts.cryptoCallbacks?.processKeyCounts(
1584
- data.device_one_time_keys_count,
1585
- data.device_unused_fallback_key_types ?? data["org.matrix.msc2732.device_unused_fallback_key_types"],
1586
- );
1587
1575
  }
1588
1576
 
1589
1577
  /**
@@ -2001,6 +1989,41 @@ export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts:
2001
1989
  return room;
2002
1990
  }
2003
1991
 
1992
+ /**
1993
+ * Pass the encryption-relevant parts of a sync response to the crypto layer, and dispatch the resulting to-device
1994
+ * messages on the client.
1995
+ *
1996
+ * `changes.toDeviceEvents` is first filtered with {@link noUnsafeEventProps}. If crypto is not enabled, the to-device
1997
+ * messages are dispatched as received.
1998
+ */
1999
+ export async function processSyncCryptoChanges(
2000
+ client: MatrixClient,
2001
+ cryptoCallbacks: SyncCryptoCallbacks | undefined,
2002
+ changes: SyncCryptoChanges,
2003
+ ): Promise<void> {
2004
+ const toDeviceEvents = changes.toDeviceEvents.filter(noUnsafeEventProps);
2005
+
2006
+ let receivedToDeviceMessages: ReceivedToDeviceMessage[];
2007
+ if (cryptoCallbacks) {
2008
+ try {
2009
+ receivedToDeviceMessages = await cryptoCallbacks.processSyncChanges({ ...changes, toDeviceEvents });
2010
+ } catch (e) {
2011
+ // Don't let a failure in the crypto layer stop the rest of the sync response from being processed: the
2012
+ // sync token has already been advanced, so the room data would otherwise be lost.
2013
+ logger.error("Error passing sync changes to the crypto layer", e);
2014
+ return;
2015
+ }
2016
+ } else {
2017
+ // Crypto is not enabled, so we just return the events.
2018
+ //
2019
+ // FIXME if we *don't* have a crypto module, we still need to invalidate the device lists. But that would
2020
+ // require a substantial bit of rework :/.
2021
+ receivedToDeviceMessages = toDeviceEvents.map((message) => ({ message, encryptionInfo: null }));
2022
+ }
2023
+
2024
+ processToDeviceMessages(receivedToDeviceMessages, client);
2025
+ }
2026
+
2004
2027
  /**
2005
2028
  * Process a list of (decrypted, where possible) received to-device events.
2006
2029
  *