@stream-io/video-client 1.57.0 → 1.59.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 (130) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/e2ee-worker.browser.es.js +1342 -0
  3. package/dist/e2ee-worker.browser.es.js.map +1 -0
  4. package/dist/index.browser.es.js +1141 -117
  5. package/dist/index.browser.es.js.map +1 -1
  6. package/dist/index.cjs.js +1196 -115
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.es.js +1174 -117
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/src/Call.d.ts +66 -9
  12. package/dist/src/StreamVideoClient.d.ts +9 -1
  13. package/dist/src/coordinator/connection/connection.d.ts +1 -1
  14. package/dist/src/devices/CameraManager.d.ts +8 -1
  15. package/dist/src/devices/DeviceManager.d.ts +2 -1
  16. package/dist/src/devices/MicrophoneManager.d.ts +8 -1
  17. package/dist/src/devices/SpeakerManager.d.ts +2 -1
  18. package/dist/src/events/participant.d.ts +10 -3
  19. package/dist/src/gen/coordinator/index.d.ts +1386 -240
  20. package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
  21. package/dist/src/helpers/MediaStreamRecorder.d.ts +26 -0
  22. package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
  23. package/dist/src/helpers/concurrency.d.ts +8 -0
  24. package/dist/src/helpers/ensureExhausted.d.ts +1 -1
  25. package/dist/src/helpers/loopback.d.ts +55 -0
  26. package/dist/src/permissions/PermissionsContext.d.ts +2 -2
  27. package/dist/src/reporting/ClientEventReporter.d.ts +0 -2
  28. package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
  29. package/dist/src/rtc/Subscriber.d.ts +5 -0
  30. package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
  31. package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
  32. package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
  33. package/dist/src/rtc/e2ee/events.d.ts +163 -0
  34. package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
  35. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
  36. package/dist/src/rtc/index.d.ts +1 -0
  37. package/dist/src/rtc/mediaEngine.d.ts +23 -0
  38. package/dist/src/rtc/types.d.ts +2 -0
  39. package/dist/src/store/CallState.d.ts +21 -0
  40. package/dist/src/types.d.ts +15 -6
  41. package/index.ts +5 -0
  42. package/package.json +1 -1
  43. package/src/Call.ts +311 -155
  44. package/src/StreamVideoClient.ts +35 -8
  45. package/src/__tests__/Call.lifecycle.test.ts +33 -2
  46. package/src/__tests__/Call.publishing.test.ts +42 -1
  47. package/src/__tests__/Call.test.ts +15 -0
  48. package/src/__tests__/StreamVideoClient.api.test.ts +33 -0
  49. package/src/__tests__/StreamVideoClient.rewatch.test.ts +213 -0
  50. package/src/__tests__/StreamVideoClient.ringing.test.ts +11 -4
  51. package/src/__tests__/StreamVideoClient.test.ts +14 -12
  52. package/src/coordinator/connection/connection.ts +6 -3
  53. package/src/devices/AudioDeviceManager.ts +6 -0
  54. package/src/devices/CameraManager.ts +67 -8
  55. package/src/devices/DeviceManager.ts +24 -4
  56. package/src/devices/MicrophoneManager.ts +66 -8
  57. package/src/devices/ScreenShareManager.ts +3 -0
  58. package/src/devices/SpeakerManager.ts +42 -12
  59. package/src/devices/__tests__/CameraManager.test.ts +27 -1
  60. package/src/devices/__tests__/DeviceManager.test.ts +49 -1
  61. package/src/devices/__tests__/MicrophoneManager.test.ts +23 -1
  62. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
  63. package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
  64. package/src/devices/__tests__/mocks.ts +36 -3
  65. package/src/events/__tests__/participant.test.ts +134 -1
  66. package/src/events/call.ts +6 -3
  67. package/src/events/callEventHandlers.ts +9 -3
  68. package/src/events/participant.ts +42 -7
  69. package/src/gen/coordinator/index.ts +1402 -236
  70. package/src/gen/video/sfu/models/models.ts +15 -0
  71. package/src/helpers/MediaStreamRecorder.ts +104 -0
  72. package/src/helpers/TypedEventEmitter.ts +71 -0
  73. package/src/helpers/__tests__/MediaStreamRecorder.test.ts +324 -0
  74. package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
  75. package/src/helpers/__tests__/concurrency.test.ts +74 -0
  76. package/src/helpers/__tests__/loopback.test.ts +367 -0
  77. package/src/helpers/concurrency.ts +24 -0
  78. package/src/helpers/ensureExhausted.ts +4 -2
  79. package/src/helpers/loopback.ts +200 -0
  80. package/src/permissions/PermissionsContext.ts +10 -5
  81. package/src/reporting/ClientEventReporter.ts +15 -28
  82. package/src/reporting/__tests__/ClientEventReporter.test.ts +3 -7
  83. package/src/rtc/BasePeerConnection.ts +12 -1
  84. package/src/rtc/Publisher.ts +8 -0
  85. package/src/rtc/Subscriber.ts +13 -0
  86. package/src/rtc/__tests__/Publisher.test.ts +101 -0
  87. package/src/rtc/__tests__/Subscriber.test.ts +190 -0
  88. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
  89. package/src/rtc/e2ee/E2EEManager.ts +12 -0
  90. package/src/rtc/e2ee/EncryptionManager.ts +336 -0
  91. package/src/rtc/e2ee/SPEC.md +586 -0
  92. package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
  93. package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
  94. package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
  95. package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
  96. package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
  97. package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
  98. package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
  99. package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
  100. package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
  101. package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
  102. package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
  103. package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
  104. package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
  105. package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
  106. package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
  107. package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
  108. package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
  109. package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
  110. package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
  111. package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
  112. package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
  113. package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
  114. package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
  115. package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
  116. package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
  117. package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
  118. package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
  119. package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
  120. package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
  121. package/src/rtc/e2ee/e2ee-worker.ts +27 -0
  122. package/src/rtc/e2ee/events.ts +179 -0
  123. package/src/rtc/e2ee/transformSupport.ts +35 -0
  124. package/src/rtc/helpers/rtcConfiguration.ts +4 -2
  125. package/src/rtc/index.ts +1 -0
  126. package/src/rtc/mediaEngine.ts +44 -0
  127. package/src/rtc/types.ts +2 -0
  128. package/src/store/CallState.ts +33 -1
  129. package/src/store/__tests__/CallState.test.ts +82 -1
  130. package/src/types.ts +23 -5
@@ -57,6 +57,7 @@ const RTCRtpTransceiverMock = vi.fn(function (): Partial<RTCRtpTransceiver> {
57
57
  replaceTrack: vi.fn(),
58
58
  getParameters: vi.fn().mockReturnValue({}),
59
59
  setParameters: vi.fn(),
60
+ transform: null,
60
61
  },
61
62
  setCodecPreferences: vi.fn(),
62
63
  mid: '',
@@ -80,6 +81,7 @@ const RTCRtpReceiverMock = vi.fn(function (): Partial<typeof RTCRtpReceiver> {
80
81
  getCapabilities: vi.fn(),
81
82
  };
82
83
  });
84
+ RTCRtpReceiverMock.prototype.transform = null;
83
85
  vi.stubGlobal('RTCRtpReceiver', RTCRtpReceiverMock);
84
86
 
85
87
  const RTCRtpSenderMock = vi.fn(function (): Partial<typeof RTCRtpSender> {
@@ -89,8 +91,37 @@ const RTCRtpSenderMock = vi.fn(function (): Partial<typeof RTCRtpSender> {
89
91
  track: vi.fn(),
90
92
  };
91
93
  });
94
+ RTCRtpSenderMock.prototype.transform = null;
92
95
  vi.stubGlobal('RTCRtpSender', RTCRtpSenderMock);
93
96
 
97
+ const WorkerMock = vi.fn(function (): Partial<Worker> {
98
+ return {
99
+ postMessage: vi.fn(),
100
+ terminate: vi.fn(),
101
+ addEventListener: vi.fn(),
102
+ removeEventListener: vi.fn(),
103
+ };
104
+ });
105
+ vi.stubGlobal('Worker', WorkerMock);
106
+
107
+ const RTCRtpScriptTransformMock = vi.fn(function (
108
+ worker: Worker,
109
+ options?: unknown,
110
+ ): Partial<RTCRtpScriptTransform> {
111
+ return {
112
+ worker,
113
+ options,
114
+ };
115
+ });
116
+ vi.stubGlobal('RTCRtpScriptTransform', RTCRtpScriptTransformMock);
117
+
118
+ if (typeof URL !== 'undefined' && typeof URL.createObjectURL !== 'function') {
119
+ Object.defineProperty(URL, 'createObjectURL', {
120
+ configurable: true,
121
+ value: vi.fn(() => 'blob:stream-video-e2ee'),
122
+ });
123
+ }
124
+
94
125
  const AudioContextMock = vi.fn(function (): Partial<AudioContext> {
95
126
  return {
96
127
  state: 'suspended',
@@ -0,0 +1,12 @@
1
+ /**
2
+ * What the RTC layer needs to attach E2EE to a track. {@link EncryptionManager}
3
+ * is the built-in AES-GCM implementation, but `Call.setE2EEManager` takes any:
4
+ * an integrator can plug in another scheme, RFC 9605 SFrame for example, by
5
+ * attaching their own encoded transform in these two methods.
6
+ */
7
+ export interface E2EEManager {
8
+ /** `trackType` only groups perf stats, keeping a camera and screen share apart. */
9
+ encrypt(sender: RTCRtpSender, codec?: string, trackType?: string): void;
10
+ /** `trackType` only groups perf stats, keeping a peer's audio and video apart. */
11
+ decrypt(receiver: RTCRtpReceiver, userId: string, trackType?: string): void;
12
+ }
@@ -0,0 +1,336 @@
1
+ import { preferredTransform } from './transformSupport';
2
+ import { TypedEventEmitter } from '../../helpers/TypedEventEmitter';
3
+ import type { E2EEEventMap } from './events';
4
+ import type { E2EEManager } from './E2EEManager';
5
+
6
+ export type {
7
+ E2EEEventMap,
8
+ DecryptionFailedEvent,
9
+ DecryptionResumedEvent,
10
+ DecryptionStalledEvent,
11
+ EncryptionFailedEvent,
12
+ KeyStateReport,
13
+ MissingKeyEvent,
14
+ PerfReport,
15
+ TrackPerf,
16
+ UnencryptedFrameEvent,
17
+ UnsupportedVersionEvent,
18
+ } from './events';
19
+
20
+ /**
21
+ * - `'AES-128-GCM'` (default): 16-byte keys, per BSI TR-02102-1 §3.2.
22
+ * - `'AES-256-GCM'`: 32-byte keys. For a compliance reviewer that requires
23
+ * 256-bit strength, such as a KBV Anlage 31b certifier.
24
+ */
25
+ export type E2EEAlgorithm = 'AES-128-GCM' | 'AES-256-GCM';
26
+
27
+ /** Options for {@link EncryptionManager.create}. */
28
+ export type EncryptionManagerOptions = {
29
+ algorithm?: E2EEAlgorithm;
30
+ };
31
+
32
+ /**
33
+ * Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
34
+ * transforms to RTCRtpSenders and RTCRtpReceivers.
35
+ */
36
+ export class EncryptionManager
37
+ extends TypedEventEmitter<E2EEEventMap>
38
+ implements E2EEManager
39
+ {
40
+ private readonly algorithm: E2EEAlgorithm;
41
+ private readonly transform: 'script' | 'insertable';
42
+ private disposed = false;
43
+ private piped?: WeakSet<RTCRtpSender | RTCRtpReceiver>;
44
+
45
+ private readonly userId: string;
46
+ private readonly worker: Worker;
47
+ private readonly workerUrl: string;
48
+
49
+ private constructor(
50
+ userId: string,
51
+ worker: Worker,
52
+ workerUrl: string,
53
+ algorithm: E2EEAlgorithm,
54
+ transform: 'script' | 'insertable',
55
+ ) {
56
+ super('EncryptionManager');
57
+ this.userId = userId;
58
+ this.worker = worker;
59
+ this.workerUrl = workerUrl;
60
+ this.algorithm = algorithm;
61
+ this.transform = transform;
62
+ this.worker.addEventListener('message', this.handleWorkerMessage);
63
+ this.worker.addEventListener('error', this.handleWorkerError);
64
+ }
65
+
66
+ /**
67
+ * Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
68
+ * Use it to guard UI, or to avoid calling {@link create} where it would throw.
69
+ */
70
+ static isSupported = (): boolean => {
71
+ return preferredTransform() !== undefined;
72
+ };
73
+
74
+ /**
75
+ * Create an EncryptionManager instance and initialize the E2EE Web Worker.
76
+ *
77
+ * @param userId - The local user's ID, normally `call.currentUserId`.
78
+ * @param options - the create options.
79
+ * @throws {Error} If the browser lacks Encoded Transforms.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * if (EncryptionManager.isSupported()) {
84
+ * const e2ee = await EncryptionManager.create(call.currentUserId);
85
+ * call.setE2EEManager(e2ee);
86
+ * e2ee.setSharedKey(0, keyBytes);
87
+ * }
88
+ */
89
+ static create = async (
90
+ userId: string,
91
+ options?: EncryptionManagerOptions,
92
+ ): Promise<EncryptionManager> => {
93
+ const transform = preferredTransform();
94
+ if (!transform) {
95
+ throw new Error(`E2EE is not supported in this browser`);
96
+ }
97
+ const { e2eeWorker } = await import('./e2ee-worker');
98
+ const blob = new Blob([`(${e2eeWorker.toString()})()`], {
99
+ type: 'application/javascript',
100
+ });
101
+ const url = URL.createObjectURL(blob);
102
+ let worker: Worker;
103
+ try {
104
+ worker = new Worker(url, { name: 'stream-video-e2ee' });
105
+ } catch (err) {
106
+ // e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
107
+ URL.revokeObjectURL(url);
108
+ throw err;
109
+ }
110
+ const algorithm = options?.algorithm ?? 'AES-128-GCM';
111
+ return new EncryptionManager(userId, worker, url, algorithm, transform);
112
+ };
113
+
114
+ /**
115
+ * {@link dispose} terminates the worker, so `postMessage` becomes a silent
116
+ * no-op and an attached transform points at a dead worker: frames would stall
117
+ * forever with no error and no event. Throwing is also fail-closed, since a
118
+ * caller that swallows it still publishes nothing rather than cleartext.
119
+ */
120
+ private assertUsable = () => {
121
+ if (this.disposed) throw new Error(`EncryptionManager is disposed`);
122
+ };
123
+
124
+ /**
125
+ * Terminate the worker and release all resources.
126
+ *
127
+ * The manager is unusable afterwards and every other method throws. Call
128
+ * {@link create} for a new one. Safe to call more than once.
129
+ */
130
+ dispose = (): void => {
131
+ if (this.disposed) return;
132
+ this.disposed = true;
133
+ this.piped = undefined;
134
+ this.worker.removeEventListener('message', this.handleWorkerMessage);
135
+ this.worker.removeEventListener('error', this.handleWorkerError);
136
+ this.worker.terminate();
137
+ URL.revokeObjectURL(this.workerUrl);
138
+ this.removeAllListeners();
139
+ };
140
+
141
+ /**
142
+ * Set a per-user AES-GCM encryption key in the worker's key store.
143
+ *
144
+ * Use it when each participant has their own key from a central authority.
145
+ * The receiver picks the right one by the `keyIndex` in the frame trailer.
146
+ *
147
+ * @param userId - The key owner.
148
+ * @param keyIndex - Increases with each rotation.
149
+ * @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
150
+ */
151
+ setKey = (userId: string, keyIndex: number, rawKey: ArrayBuffer): void => {
152
+ this.assertUsable();
153
+ this.validateKeyIndex(keyIndex);
154
+ this.validateKeyLength(rawKey);
155
+ this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
156
+ };
157
+
158
+ /**
159
+ * Fallback key for any user without a per-user key. The simplest E2EE mode:
160
+ * one key for everyone, usually passphrase-derived, no distribution needed.
161
+ * Setting an epoch makes it active for encryption while older epochs remain
162
+ * available to decrypt in-flight frames until {@link removeSharedKey}.
163
+ *
164
+ * @param keyIndex - An integer 0-255, since one trailer byte carries it.
165
+ * @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
166
+ */
167
+ setSharedKey = (keyIndex: number, rawKey: ArrayBuffer): void => {
168
+ this.assertUsable();
169
+ this.validateKeyIndex(keyIndex);
170
+ this.validateKeyLength(rawKey);
171
+ this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
172
+ };
173
+
174
+ /**
175
+ * Remove one shared-key epoch from the worker's receive key ring.
176
+ *
177
+ * If this is the active epoch, shared-key encryption stops until
178
+ * {@link setSharedKey} succeeds again. An older epoch is not reactivated.
179
+ *
180
+ * @param keyIndex - The exact shared-key epoch to remove.
181
+ */
182
+ removeSharedKey = (keyIndex: number): void => {
183
+ this.assertUsable();
184
+ this.validateKeyIndex(keyIndex);
185
+ this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
186
+ };
187
+
188
+ /**
189
+ * Retire one of a user's key epochs, leaving their other epochs usable.
190
+ *
191
+ * @param userId - The key owner.
192
+ * @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
193
+ */
194
+ removeKey = (userId: string, keyIndex: number): void => {
195
+ this.assertUsable();
196
+ this.validateKeyIndex(keyIndex);
197
+ this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
198
+ };
199
+
200
+ /**
201
+ * Drop every key a user holds, revoking their ability to decrypt later
202
+ * frames. Call it when a participant leaves.
203
+ *
204
+ * To retire one rotated epoch instead, use {@link removeKey}.
205
+ */
206
+ removeAllKeys = (userId: string): void => {
207
+ this.assertUsable();
208
+ this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
209
+ };
210
+
211
+ /**
212
+ * Called by the Publisher when it adds a transceiver.
213
+ *
214
+ * @param sender - The sender to encrypt.
215
+ * @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
216
+ * @param trackType - Optional label; only groups perf stats.
217
+ * @internal
218
+ */
219
+ encrypt = (
220
+ sender: RTCRtpSender,
221
+ codec?: string,
222
+ trackType?: string,
223
+ ): void => {
224
+ this.assertUsable();
225
+ this.pipe(sender, {
226
+ operation: 'encode',
227
+ userId: this.userId,
228
+ codec,
229
+ trackType,
230
+ });
231
+ };
232
+
233
+ /**
234
+ * Called by the Subscriber when a remote track arrives.
235
+ *
236
+ * @param receiver - The receiver to decrypt.
237
+ * @param userId - The remote user, for key lookup in the worker.
238
+ * @param trackType - Optional label; only groups perf stats.
239
+ * @internal
240
+ */
241
+ decrypt = (
242
+ receiver: RTCRtpReceiver,
243
+ userId: string,
244
+ trackType?: string,
245
+ ): void => {
246
+ this.assertUsable();
247
+ this.pipe(receiver, { operation: 'decode', userId, trackType });
248
+ };
249
+
250
+ /** Pipe through the worker's transform, tracking targets to avoid double-piping. */
251
+ private pipe = (
252
+ target: RTCRtpSender | RTCRtpReceiver,
253
+ options: {
254
+ operation: string;
255
+ userId: string;
256
+ codec?: string;
257
+ trackType?: string;
258
+ },
259
+ ): void => {
260
+ if (this.transform === 'script') {
261
+ target.transform = new RTCRtpScriptTransform(this.worker, options);
262
+ return;
263
+ }
264
+
265
+ if ((this.piped ??= new WeakSet()).has(target)) return;
266
+ this.piped.add(target);
267
+ // @ts-expect-error createEncodedStreams is not in the standard typedefs
268
+ const { readable, writable } = target.createEncodedStreams();
269
+ this.worker.postMessage(
270
+ { type: 'cmd.setup_transform', ...options, readable, writable },
271
+ [readable, writable],
272
+ );
273
+ };
274
+
275
+ /**
276
+ * Toggle periodic performance reporting from the E2EE worker.
277
+ *
278
+ * While on, the worker emits `e2ee.perf_report` once per second with per-track
279
+ * FPS and crypto timings. Useful for debugging throughput.
280
+ */
281
+ enablePerformanceReporting = (enabled: boolean): void => {
282
+ this.assertUsable();
283
+ this.worker.postMessage({
284
+ type: 'cmd.enable_performance_reporting',
285
+ enabled,
286
+ });
287
+ };
288
+
289
+ /**
290
+ * Request a snapshot of the worker's keys. It arrives later as the
291
+ * `e2ee.key_state` event, listing fingerprints only, never key material.
292
+ */
293
+ requestKeyState = (): void => {
294
+ this.assertUsable();
295
+ this.worker.postMessage({ type: 'cmd.request_key_state' });
296
+ };
297
+
298
+ private handleWorkerMessage = (e: MessageEvent) => {
299
+ const { type, ...payload } = e.data ?? {};
300
+ if (type === 'e2ee.error') {
301
+ this.logger.error(e.data.message);
302
+ return;
303
+ }
304
+ const event = type as keyof E2EEEventMap;
305
+ this.logger.debug('Dispatching', event, payload);
306
+
307
+ this.emit(event, payload as never);
308
+ };
309
+
310
+ private handleWorkerError = (e: ErrorEvent) => {
311
+ this.logger.error('Unhandled worker error:', e.message);
312
+ };
313
+
314
+ private validateKeyLength = (rawKey: ArrayBuffer) => {
315
+ const is256 = this.algorithm === 'AES-256-GCM';
316
+ const expected = is256 ? 32 : 16;
317
+ if (rawKey.byteLength !== expected) {
318
+ throw new Error(
319
+ `Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`,
320
+ );
321
+ }
322
+ };
323
+
324
+ /**
325
+ * One trailer byte carries the keyIndex. A larger value would truncate to
326
+ * `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
327
+ * every decrypt. Reject it rather than ship a silently broken key epoch.
328
+ */
329
+ private validateKeyIndex = (keyIndex: number) => {
330
+ if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
331
+ throw new Error(
332
+ `keyIndex must be an integer between 0 and 255, got ${keyIndex}`,
333
+ );
334
+ }
335
+ };
336
+ }