@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.
- package/CHANGELOG.md +13 -0
- package/dist/e2ee-worker.browser.es.js +1342 -0
- package/dist/e2ee-worker.browser.es.js.map +1 -0
- package/dist/index.browser.es.js +831 -55
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +875 -54
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +864 -55
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +52 -6
- package/dist/src/coordinator/connection/connection.d.ts +1 -1
- package/dist/src/devices/CameraManager.d.ts +6 -0
- package/dist/src/devices/DeviceManager.d.ts +1 -0
- package/dist/src/devices/MicrophoneManager.d.ts +6 -0
- package/dist/src/devices/SpeakerManager.d.ts +2 -1
- package/dist/src/events/participant.d.ts +10 -3
- package/dist/src/gen/coordinator/index.d.ts +1363 -243
- package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
- package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
- package/dist/src/helpers/ensureExhausted.d.ts +1 -1
- package/dist/src/permissions/PermissionsContext.d.ts +2 -2
- package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
- package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
- package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
- package/dist/src/rtc/e2ee/events.d.ts +163 -0
- package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/mediaEngine.d.ts +23 -0
- package/dist/src/rtc/types.d.ts +2 -0
- package/dist/src/store/CallState.d.ts +21 -0
- package/dist/src/types.d.ts +15 -6
- package/index.ts +3 -0
- package/package.json +1 -1
- package/src/Call.ts +160 -26
- package/src/__tests__/Call.test.ts +15 -0
- package/src/coordinator/connection/connection.ts +6 -3
- package/src/devices/AudioDeviceManager.ts +6 -0
- package/src/devices/CameraManager.ts +51 -4
- package/src/devices/DeviceManager.ts +17 -0
- package/src/devices/MicrophoneManager.ts +55 -5
- package/src/devices/ScreenShareManager.ts +3 -0
- package/src/devices/SpeakerManager.ts +42 -12
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
- package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
- package/src/devices/__tests__/mocks.ts +36 -3
- package/src/events/__tests__/participant.test.ts +134 -1
- package/src/events/callEventHandlers.ts +9 -3
- package/src/events/participant.ts +42 -7
- package/src/gen/coordinator/index.ts +1384 -244
- package/src/gen/video/sfu/models/models.ts +15 -0
- package/src/helpers/TypedEventEmitter.ts +71 -0
- package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
- package/src/helpers/ensureExhausted.ts +4 -2
- package/src/permissions/PermissionsContext.ts +10 -5
- package/src/rtc/BasePeerConnection.ts +12 -1
- package/src/rtc/Publisher.ts +8 -0
- package/src/rtc/Subscriber.ts +5 -0
- package/src/rtc/__tests__/Publisher.test.ts +101 -0
- package/src/rtc/__tests__/Subscriber.test.ts +89 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
- package/src/rtc/e2ee/E2EEManager.ts +12 -0
- package/src/rtc/e2ee/EncryptionManager.ts +336 -0
- package/src/rtc/e2ee/SPEC.md +586 -0
- package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
- package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
- package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
- package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
- package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
- package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
- package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
- package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
- package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
- package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
- package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
- package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
- package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
- package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
- package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
- package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
- package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
- package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
- package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
- package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
- package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
- package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
- package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
- package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
- package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
- package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
- package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
- package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
- package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
- package/src/rtc/e2ee/e2ee-worker.ts +27 -0
- package/src/rtc/e2ee/events.ts +179 -0
- package/src/rtc/e2ee/transformSupport.ts +35 -0
- package/src/rtc/helpers/rtcConfiguration.ts +4 -2
- package/src/rtc/index.ts +1 -0
- package/src/rtc/mediaEngine.ts +44 -0
- package/src/rtc/types.ts +2 -0
- package/src/store/CallState.ts +33 -1
- package/src/store/__tests__/CallState.test.ts +82 -1
- 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
|
+
});
|