@stream-io/video-client 1.57.0 → 1.58.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 (105) hide show
  1. package/CHANGELOG.md +13 -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 +831 -55
  5. package/dist/index.browser.es.js.map +1 -1
  6. package/dist/index.cjs.js +875 -54
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.es.js +864 -55
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/src/Call.d.ts +52 -6
  12. package/dist/src/coordinator/connection/connection.d.ts +1 -1
  13. package/dist/src/devices/CameraManager.d.ts +6 -0
  14. package/dist/src/devices/DeviceManager.d.ts +1 -0
  15. package/dist/src/devices/MicrophoneManager.d.ts +6 -0
  16. package/dist/src/devices/SpeakerManager.d.ts +2 -1
  17. package/dist/src/events/participant.d.ts +10 -3
  18. package/dist/src/gen/coordinator/index.d.ts +1363 -243
  19. package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
  20. package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
  21. package/dist/src/helpers/ensureExhausted.d.ts +1 -1
  22. package/dist/src/permissions/PermissionsContext.d.ts +2 -2
  23. package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
  24. package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
  25. package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
  26. package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
  27. package/dist/src/rtc/e2ee/events.d.ts +163 -0
  28. package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
  29. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
  30. package/dist/src/rtc/index.d.ts +1 -0
  31. package/dist/src/rtc/mediaEngine.d.ts +23 -0
  32. package/dist/src/rtc/types.d.ts +2 -0
  33. package/dist/src/store/CallState.d.ts +21 -0
  34. package/dist/src/types.d.ts +15 -6
  35. package/index.ts +3 -0
  36. package/package.json +1 -1
  37. package/src/Call.ts +160 -26
  38. package/src/__tests__/Call.test.ts +15 -0
  39. package/src/coordinator/connection/connection.ts +6 -3
  40. package/src/devices/AudioDeviceManager.ts +6 -0
  41. package/src/devices/CameraManager.ts +51 -4
  42. package/src/devices/DeviceManager.ts +17 -0
  43. package/src/devices/MicrophoneManager.ts +55 -5
  44. package/src/devices/ScreenShareManager.ts +3 -0
  45. package/src/devices/SpeakerManager.ts +42 -12
  46. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
  47. package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
  48. package/src/devices/__tests__/mocks.ts +36 -3
  49. package/src/events/__tests__/participant.test.ts +134 -1
  50. package/src/events/callEventHandlers.ts +9 -3
  51. package/src/events/participant.ts +42 -7
  52. package/src/gen/coordinator/index.ts +1384 -244
  53. package/src/gen/video/sfu/models/models.ts +15 -0
  54. package/src/helpers/TypedEventEmitter.ts +71 -0
  55. package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
  56. package/src/helpers/ensureExhausted.ts +4 -2
  57. package/src/permissions/PermissionsContext.ts +10 -5
  58. package/src/rtc/BasePeerConnection.ts +12 -1
  59. package/src/rtc/Publisher.ts +8 -0
  60. package/src/rtc/Subscriber.ts +5 -0
  61. package/src/rtc/__tests__/Publisher.test.ts +101 -0
  62. package/src/rtc/__tests__/Subscriber.test.ts +89 -0
  63. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
  64. package/src/rtc/e2ee/E2EEManager.ts +12 -0
  65. package/src/rtc/e2ee/EncryptionManager.ts +336 -0
  66. package/src/rtc/e2ee/SPEC.md +586 -0
  67. package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
  68. package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
  69. package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
  70. package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
  71. package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
  72. package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
  73. package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
  74. package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
  75. package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
  76. package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
  77. package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
  78. package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
  79. package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
  80. package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
  81. package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
  82. package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
  83. package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
  84. package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
  85. package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
  86. package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
  87. package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
  88. package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
  89. package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
  90. package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
  91. package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
  92. package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
  93. package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
  94. package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
  95. package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
  96. package/src/rtc/e2ee/e2ee-worker.ts +27 -0
  97. package/src/rtc/e2ee/events.ts +179 -0
  98. package/src/rtc/e2ee/transformSupport.ts +35 -0
  99. package/src/rtc/helpers/rtcConfiguration.ts +4 -2
  100. package/src/rtc/index.ts +1 -0
  101. package/src/rtc/mediaEngine.ts +44 -0
  102. package/src/rtc/types.ts +2 -0
  103. package/src/store/CallState.ts +33 -1
  104. package/src/store/__tests__/CallState.test.ts +82 -1
  105. package/src/types.ts +23 -5
@@ -0,0 +1,133 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { COUNTER_HARD_LIMIT, REPLAY_WINDOW } from '../e2ee-worker/constants';
3
+ import { ReplayWindow } from '../e2ee-worker/replayWindow';
4
+
5
+ describe('ReplayWindow', () => {
6
+ const PREFIX_A = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
7
+ const PREFIX_B = new Uint8Array([9, 9, 9, 9, 9, 9, 9, 9]);
8
+ const PREFIX_C = new Uint8Array([3, 3, 3, 3, 3, 3, 3, 3]);
9
+ const PREFIX_D = new Uint8Array([4, 4, 4, 4, 4, 4, 4, 4]);
10
+
11
+ // Mirrors the real decode flow: a frame is only recorded (commit) once it
12
+ // would have authenticated. Returns whether the window admitted it.
13
+ const accept = (w: ReplayWindow, counter: number, prefix: Uint8Array) => {
14
+ const ok = w.peek(counter, prefix);
15
+ if (ok) w.commit(counter, prefix);
16
+ return ok;
17
+ };
18
+
19
+ it('accepts monotonically increasing counters', () => {
20
+ const w = new ReplayWindow();
21
+ expect(accept(w, 1, PREFIX_A)).toBe(true);
22
+ expect(accept(w, 2, PREFIX_A)).toBe(true);
23
+ expect(accept(w, 3, PREFIX_A)).toBe(true);
24
+ });
25
+
26
+ it('rejects an exact replay', () => {
27
+ const w = new ReplayWindow();
28
+ expect(accept(w, 5, PREFIX_A)).toBe(true);
29
+ expect(accept(w, 5, PREFIX_A)).toBe(false);
30
+ });
31
+
32
+ it('accepts out-of-order frames within the window', () => {
33
+ const w = new ReplayWindow();
34
+ expect(accept(w, 10, PREFIX_A)).toBe(true);
35
+ expect(accept(w, 8, PREFIX_A)).toBe(true); // late arrival
36
+ expect(accept(w, 8, PREFIX_A)).toBe(false); // replay of late arrival
37
+ });
38
+
39
+ it('rejects frames older than the replay window', () => {
40
+ const w = new ReplayWindow();
41
+ const high = REPLAY_WINDOW + 50;
42
+ expect(accept(w, high, PREFIX_A)).toBe(true);
43
+ expect(accept(w, 1, PREFIX_A)).toBe(false);
44
+ expect(accept(w, high - REPLAY_WINDOW, PREFIX_A)).toBe(false);
45
+ });
46
+
47
+ it('isolates state per track (the M1 fix)', () => {
48
+ // Each decode transform owns its own guard, so one track racing far
49
+ // ahead in counter terms can never evict a slower track's frames — the
50
+ // failure mode of the old shared (userId, keyIndex) window.
51
+ const audio = new ReplayWindow();
52
+ const video = new ReplayWindow();
53
+ expect(accept(audio, REPLAY_WINDOW * 4, PREFIX_A)).toBe(true);
54
+ expect(accept(video, 5, PREFIX_A)).toBe(true);
55
+ expect(accept(video, 6, PREFIX_A)).toBe(true);
56
+ });
57
+
58
+ it('partitions the window by sender prefix, so a restart is not rejected', () => {
59
+ // A sender restart or key re-import brings a fresh prefix and a counter
60
+ // near 0. Those low counters must not be judged against the old prefix's
61
+ // high-water mark, while replays within each prefix are still caught.
62
+ const w = new ReplayWindow();
63
+ expect(accept(w, 5000, PREFIX_A)).toBe(true);
64
+ expect(accept(w, 1, PREFIX_B)).toBe(true);
65
+ expect(accept(w, 2, PREFIX_B)).toBe(true);
66
+ expect(accept(w, 5000, PREFIX_A)).toBe(false); // replay within prefix A
67
+ expect(accept(w, 1, PREFIX_B)).toBe(false); // replay within prefix B
68
+ });
69
+
70
+ // --- authenticate-before-commit -----------------------------------------
71
+
72
+ it('peek is read-only — a forged high counter cannot wedge the track', () => {
73
+ const w = new ReplayWindow();
74
+ // A genuine frame establishes the window.
75
+ expect(accept(w, 10, PREFIX_A)).toBe(true);
76
+ // Forged frames copy the prefix and claim far-future counters. They peek
77
+ // OK (nothing seen is newer), but GCM rejects them, so they are never
78
+ // committed and `highest` must not move: otherwise every later genuine
79
+ // frame lands below `highest - REPLAY_WINDOW` and is dropped forever.
80
+ expect(w.peek(COUNTER_HARD_LIMIT, PREFIX_A)).toBe(true);
81
+ expect(w.peek(900_000, PREFIX_A)).toBe(true);
82
+ expect(w.peek(900_000, PREFIX_A)).toBe(true);
83
+ expect(accept(w, 11, PREFIX_A)).toBe(true);
84
+ expect(accept(w, 12, PREFIX_A)).toBe(true);
85
+ // The uncommitted peeks also left no epoch behind, so a genuine low
86
+ // counter is still new rather than a replay.
87
+ expect(accept(w, 1, PREFIX_A)).toBe(true);
88
+ expect(accept(w, 1, PREFIX_A)).toBe(false);
89
+ });
90
+
91
+ it('clears the slots an advance skipped, so a reused slot is not a false replay', () => {
92
+ // Bitmap slots repeat every REPLAY_WINDOW counters, so counter 5 and
93
+ // counter 5 + REPLAY_WINDOW share one bit. When the mark advances past
94
+ // counters that never arrived, their slots must be cleared, or a later
95
+ // genuine frame landing on one is rejected as a replay of the old counter.
96
+ const w = new ReplayWindow();
97
+ const reused = 5 + REPLAY_WINDOW; // same bitmap slot as counter 5
98
+ expect(accept(w, 5, PREFIX_A)).toBe(true);
99
+ // Advance in steps smaller than the window, so the skipped slots are
100
+ // cleared one by one rather than by wiping the whole bitmap.
101
+ expect(accept(w, REPLAY_WINDOW - 24, PREFIX_A)).toBe(true);
102
+ expect(accept(w, REPLAY_WINDOW + 76, PREFIX_A)).toBe(true);
103
+ // `reused` is a new counter, still inside the window, and its slot was
104
+ // last set by counter 5. It must be accepted.
105
+ expect(accept(w, reused, PREFIX_A)).toBe(true);
106
+ expect(accept(w, reused, PREFIX_A)).toBe(false); // now a genuine replay
107
+ });
108
+
109
+ it('handles a counter jump larger than the replay window', () => {
110
+ // Exercises the window-advance path where the whole bitmap is stale and
111
+ // must be cleared at once.
112
+ const w = new ReplayWindow();
113
+ expect(accept(w, 1, PREFIX_A)).toBe(true);
114
+ const far = 1 + REPLAY_WINDOW * 3;
115
+ expect(accept(w, far, PREFIX_A)).toBe(true); // jump well beyond the window
116
+ expect(accept(w, far, PREFIX_A)).toBe(false); // replay of the far frame
117
+ expect(accept(w, 2, PREFIX_A)).toBe(false); // now far older than the window
118
+ });
119
+
120
+ it('an uncommitted novel-prefix peek cannot evict a committed epoch', () => {
121
+ const w = new ReplayWindow();
122
+ // Authentic frame on prefix A is committed.
123
+ expect(accept(w, 5, PREFIX_A)).toBe(true);
124
+ // Attacker injects frames with distinct novel prefixes (> REPLAY_EPOCHS
125
+ // worth). They fail GCM, so they are peeked but never committed — no epoch
126
+ // is created, nothing is evicted.
127
+ for (const p of [PREFIX_B, PREFIX_C, PREFIX_D]) {
128
+ expect(w.peek(1, p)).toBe(true); // a novel prefix always peeks OK
129
+ }
130
+ // Prefix A's epoch survived, so replaying the authentic frame is caught.
131
+ expect(w.peek(5, PREFIX_A)).toBe(false);
132
+ });
133
+ });
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ IV_PREFIX_LEN,
4
+ MAX_CLEAR_BYTES,
5
+ TRAILER_LEN,
6
+ } from '../e2ee-worker/constants';
7
+ import {
8
+ readFramingVersion,
9
+ readTrailer,
10
+ writeTrailer,
11
+ } from '../e2ee-worker/trailer';
12
+
13
+ const makeFrame = (bodyLen: number): Uint8Array =>
14
+ new Uint8Array(bodyLen + TRAILER_LEN);
15
+
16
+ const randomPrefix = (): Uint8Array => {
17
+ const p = new Uint8Array(IV_PREFIX_LEN);
18
+ for (let i = 0; i < p.length; i++) p[i] = (i * 17 + 3) & 0xff;
19
+ return p;
20
+ };
21
+
22
+ describe('writeTrailer + readTrailer', () => {
23
+ it.each([
24
+ ['without the RBSP flag', false],
25
+ ['with the RBSP flag', true],
26
+ ])('round-trips the full payload %s', (_label, isRbsp) => {
27
+ const body = 32;
28
+ const dst = makeFrame(body);
29
+ const prefix = randomPrefix();
30
+ writeTrailer(dst, body, 123456, prefix, 7, 10, isRbsp);
31
+
32
+ const trailer = readTrailer(dst);
33
+ expect(trailer).not.toBeNull();
34
+ expect(trailer!.frameCounter).toBe(123456);
35
+ expect(trailer!.keyIndex).toBe(7);
36
+ expect(trailer!.clearBytes).toBe(10);
37
+ expect(trailer!.isRbsp).toBe(isRbsp);
38
+ expect(Array.from(trailer!.ivPrefix)).toEqual(Array.from(prefix));
39
+ });
40
+
41
+ it('rejects a trailer it could not encode', () => {
42
+ const dst = makeFrame(100);
43
+ expect(() =>
44
+ writeTrailer(dst, 100, 1, randomPrefix(), 0, MAX_CLEAR_BYTES + 1, false),
45
+ ).toThrow(/15-bit/);
46
+ expect(() =>
47
+ writeTrailer(dst, 20, 1, new Uint8Array(IV_PREFIX_LEN - 1), 0, 0, false),
48
+ ).toThrow(/ivPrefix/);
49
+ });
50
+
51
+ // Anything unrecognized means "not our trailer", so the frame is forwarded as
52
+ // cleartext rather than sent to a decrypt that would fail. The exact bytes of
53
+ // a valid trailer are pinned by the SPEC vectors in conformance.test.ts.
54
+ it.each([
55
+ [
56
+ 'the frame is shorter than a trailer',
57
+ () => new Uint8Array(TRAILER_LEN - 1),
58
+ ],
59
+ [
60
+ 'the magic does not match',
61
+ (f: Uint8Array) => {
62
+ f[f.length - 1] ^= 0x01;
63
+ return f;
64
+ },
65
+ ],
66
+ [
67
+ 'the version is unknown',
68
+ (f: Uint8Array) => {
69
+ f[f.length - 5] = 99;
70
+ return f;
71
+ },
72
+ ],
73
+ [
74
+ 'the declared clearBytes overruns the body',
75
+ (f: Uint8Array) => {
76
+ // Still inside the 15-bit limit, so only the length check catches it.
77
+ new DataView(f.buffer).setUint16(f.length - 7, 6);
78
+ return f;
79
+ },
80
+ ],
81
+ ])('returns null when %s', (_label, corrupt) => {
82
+ const body = 5;
83
+ const dst = makeFrame(body);
84
+ writeTrailer(dst, body, 1, randomPrefix(), 0, 0, false);
85
+ expect(readTrailer(corrupt(dst))).toBeNull();
86
+ });
87
+ });
88
+
89
+ // The identification suffix is frozen across versions, so this must keep
90
+ // working against a frame written by a version this build knows nothing about.
91
+ describe('readFramingVersion', () => {
92
+ const framed = (): Uint8Array => {
93
+ const body = 5;
94
+ const dst = makeFrame(body);
95
+ writeTrailer(dst, body, 1, randomPrefix(), 0, 0, false);
96
+ return dst;
97
+ };
98
+
99
+ // Frozen across every version (SPEC 5.2): an older receiver identifies our
100
+ // frames from these 5 bytes alone, so a layout change that moves them has to
101
+ // fail here rather than silently strand those receivers. Literal values on
102
+ // purpose - reading them from the constants would move with the break.
103
+ it('pins the identification suffix to the last 5 bytes written', () => {
104
+ const dst = framed();
105
+ const view = new DataView(dst.buffer);
106
+ expect(dst[dst.length - 5]).toBe(1);
107
+ expect(view.getUint32(dst.length - 4)).toBe(0xe2eefeed);
108
+ });
109
+
110
+ it('reports the version of a frame carrying our framing', () => {
111
+ expect(readFramingVersion(framed())).toBe(1);
112
+ });
113
+
114
+ it('accepts a caller-supplied view over the same bytes', () => {
115
+ const dst = framed();
116
+ const view = new DataView(dst.buffer, dst.byteOffset, dst.byteLength);
117
+ expect(readFramingVersion(dst, view)).toBe(readFramingVersion(dst));
118
+ });
119
+
120
+ it('reports a version this build cannot read, where readTrailer only says null', () => {
121
+ const future = framed();
122
+ future[future.length - 5] = 99;
123
+ expect(readFramingVersion(future)).toBe(99);
124
+ expect(readTrailer(future)).toBeNull();
125
+ });
126
+
127
+ it('reads the suffix from the end, so a longer future trailer still resolves', () => {
128
+ const grown = new Uint8Array(framed().length + 4);
129
+ const src = framed();
130
+ // Simulate a v2 trailer with 4 extra bytes ahead of the frozen suffix.
131
+ grown.set(src.subarray(0, src.length - 5), 0);
132
+ grown.set(src.subarray(src.length - 5), grown.length - 5);
133
+ grown[grown.length - 5] = 2;
134
+ expect(readFramingVersion(grown)).toBe(2);
135
+ });
136
+
137
+ it('returns null when the frame is not ours', () => {
138
+ const notOurs = framed();
139
+ notOurs[notOurs.length - 1] ^= 0x01;
140
+ expect(readFramingVersion(notOurs)).toBeNull();
141
+ expect(readFramingVersion(new Uint8Array(4))).toBeNull();
142
+ });
143
+ });