@blockcast/mmt-manifest-verify 0.1.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.
@@ -0,0 +1,230 @@
1
+ import { blake3 } from '@noble/hashes/blake3'
2
+
3
+ export const SOURCE_AUTH_MANIFEST_VERSION = 1
4
+ export const SOURCE_AUTH_OBJECT_ENTRY_FRAME = 1
5
+ export const SOURCE_AUTH_GROUP_CLOSE_FRAME = 2
6
+ export const SOURCE_AUTH_OBJECT_ENTRY_LENGTH = 52
7
+ export const SOURCE_AUTH_GROUP_CLOSE_LENGTH = 112
8
+ export const SOURCE_AUTH_MAX_OBJECTS_PER_GROUP = 4096
9
+ export const SOURCE_AUTH_MAX_TRUE_LENGTH = 16 * 1024 * 1024
10
+ export const SOURCE_AUTH_SCOPE_LENGTH = 32
11
+
12
+ const SIGNATURE_DOMAIN = new TextEncoder().encode('blockcast-mmt-root-v1\0\0\0')
13
+
14
+ export interface SourceAuthObjectEntry {
15
+ kind: 'object'
16
+ groupId: bigint
17
+ objectId: number
18
+ trueLength: number
19
+ digest: Uint8Array
20
+ }
21
+
22
+ export interface SourceAuthGroupClose {
23
+ kind: 'close'
24
+ groupId: bigint
25
+ objectCount: number
26
+ root: Uint8Array
27
+ signature: Uint8Array
28
+ }
29
+
30
+ export type SourceAuthManifestFrame = SourceAuthObjectEntry | SourceAuthGroupClose
31
+
32
+ function requireBytes(name: string, value: Uint8Array, length: number): void {
33
+ if (value.byteLength !== length) {
34
+ throw new Error(`${name} must be ${length} bytes, got ${value.byteLength}`)
35
+ }
36
+ }
37
+
38
+ function requireU32(name: string, value: number): void {
39
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) {
40
+ throw new Error(`${name} must be an unsigned 32-bit integer`)
41
+ }
42
+ }
43
+
44
+ function requireU64(name: string, value: bigint): void {
45
+ if (value < 0n || value > 0xffff_ffff_ffff_ffffn) {
46
+ throw new Error(`${name} must be an unsigned 64-bit integer`)
47
+ }
48
+ }
49
+
50
+ function requireTrueLength(value: number): void {
51
+ requireU32('trueLength', value)
52
+ if (value > SOURCE_AUTH_MAX_TRUE_LENGTH) {
53
+ throw new Error(`trueLength exceeds the ${SOURCE_AUTH_MAX_TRUE_LENGTH}-byte profile maximum`)
54
+ }
55
+ }
56
+
57
+ export function sourceAuthObjectDigest(object: Uint8Array): Uint8Array {
58
+ return blake3(object)
59
+ }
60
+
61
+ export function sourceAuthLeafHash(
62
+ groupId: bigint,
63
+ objectId: number,
64
+ digest: Uint8Array,
65
+ trueLength: number,
66
+ ): Uint8Array {
67
+ requireU64('groupId', groupId)
68
+ requireU32('objectId', objectId)
69
+ requireBytes('digest', digest, 32)
70
+ requireTrueLength(trueLength)
71
+ const preimage = new Uint8Array(49)
72
+ const view = new DataView(preimage.buffer)
73
+ view.setBigUint64(1, groupId)
74
+ view.setUint32(9, objectId)
75
+ preimage.set(digest, 13)
76
+ view.setUint32(45, trueLength)
77
+ return blake3(preimage)
78
+ }
79
+
80
+ export function sourceAuthMerkleRoot(
81
+ entries: readonly {
82
+ groupId: bigint
83
+ objectId: number
84
+ digest: Uint8Array
85
+ trueLength: number
86
+ }[],
87
+ ): Uint8Array {
88
+ if (entries.length === 0) throw new Error('Merkle tree requires at least one object')
89
+ if (entries.length > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {
90
+ throw new Error(`group exceeds ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP} objects`)
91
+ }
92
+ const groupId = entries[0]!.groupId
93
+ const objectIds = new Set<number>()
94
+ const ordered = [...entries].sort((left, right) => left.objectId - right.objectId)
95
+ let level = ordered.map((entry) => {
96
+ if (entry.groupId !== groupId) throw new Error('Merkle entries must belong to one group')
97
+ if (objectIds.has(entry.objectId)) throw new Error(`duplicate objectId ${entry.objectId}`)
98
+ objectIds.add(entry.objectId)
99
+ return sourceAuthLeafHash(entry.groupId, entry.objectId, entry.digest, entry.trueLength)
100
+ })
101
+ while (level.length > 1) {
102
+ const parents: Uint8Array[] = []
103
+ for (let index = 0; index < level.length; index += 2) {
104
+ const left = level[index]!
105
+ const right = level[index + 1] ?? left
106
+ const preimage = new Uint8Array(65)
107
+ preimage[0] = 1
108
+ preimage.set(left, 1)
109
+ preimage.set(right, 33)
110
+ parents.push(blake3(preimage))
111
+ }
112
+ level = parents
113
+ }
114
+ return level[0]!
115
+ }
116
+
117
+ export function trimRecoveredSourceAuthObject(
118
+ recoveredSymbol: Uint8Array,
119
+ trueLength: number,
120
+ ): Uint8Array {
121
+ requireTrueLength(trueLength)
122
+ if (trueLength > recoveredSymbol.byteLength) {
123
+ throw new Error(`true length ${trueLength} exceeds recovered object size ${recoveredSymbol.byteLength}`)
124
+ }
125
+ return recoveredSymbol.subarray(0, trueLength)
126
+ }
127
+
128
+ export function encodeSourceAuthObjectEntry(
129
+ entry: Omit<SourceAuthObjectEntry, 'kind'>,
130
+ ): Uint8Array {
131
+ requireU32('objectId', entry.objectId)
132
+ requireTrueLength(entry.trueLength)
133
+ requireU64('groupId', entry.groupId)
134
+ requireBytes('digest', entry.digest, 32)
135
+ const frame = new Uint8Array(SOURCE_AUTH_OBJECT_ENTRY_LENGTH)
136
+ const view = new DataView(frame.buffer)
137
+ frame[0] = SOURCE_AUTH_OBJECT_ENTRY_FRAME
138
+ frame[1] = SOURCE_AUTH_MANIFEST_VERSION
139
+ view.setBigUint64(4, entry.groupId)
140
+ view.setUint32(12, entry.objectId)
141
+ view.setUint32(16, entry.trueLength)
142
+ frame.set(entry.digest, 20)
143
+ return frame
144
+ }
145
+
146
+ export function encodeSourceAuthGroupClose(
147
+ close: Omit<SourceAuthGroupClose, 'kind'>,
148
+ ): Uint8Array {
149
+ requireU32('objectCount', close.objectCount)
150
+ if (close.objectCount === 0 || close.objectCount > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {
151
+ throw new Error(`objectCount must be between 1 and ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP}`)
152
+ }
153
+ requireU64('groupId', close.groupId)
154
+ requireBytes('root', close.root, 32)
155
+ requireBytes('signature', close.signature, 64)
156
+ const frame = new Uint8Array(SOURCE_AUTH_GROUP_CLOSE_LENGTH)
157
+ const view = new DataView(frame.buffer)
158
+ frame[0] = SOURCE_AUTH_GROUP_CLOSE_FRAME
159
+ frame[1] = SOURCE_AUTH_MANIFEST_VERSION
160
+ view.setBigUint64(4, close.groupId)
161
+ view.setUint32(12, close.objectCount)
162
+ frame.set(close.root, 16)
163
+ frame.set(close.signature, 48)
164
+ return frame
165
+ }
166
+
167
+ export function sourceAuthSignaturePreimage(
168
+ authScope: Uint8Array,
169
+ groupId: bigint,
170
+ objectCount: number,
171
+ root: Uint8Array,
172
+ ): Uint8Array {
173
+ requireU32('objectCount', objectCount)
174
+ if (objectCount === 0 || objectCount > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {
175
+ throw new Error(`objectCount must be between 1 and ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP}`)
176
+ }
177
+ requireU64('groupId', groupId)
178
+ requireBytes('authScope', authScope, SOURCE_AUTH_SCOPE_LENGTH)
179
+ requireBytes('root', root, 32)
180
+ const preimage = new Uint8Array(100)
181
+ const view = new DataView(preimage.buffer)
182
+ preimage.set(SIGNATURE_DOMAIN)
183
+ preimage.set(authScope, 24)
184
+ view.setBigUint64(56, groupId)
185
+ view.setUint32(64, objectCount)
186
+ preimage.set(root, 68)
187
+ return preimage
188
+ }
189
+
190
+ export function decodeSourceAuthManifestFrame(frame: Uint8Array): SourceAuthManifestFrame {
191
+ const frameType = frame[0]
192
+ const expectedLength = frameType === SOURCE_AUTH_OBJECT_ENTRY_FRAME
193
+ ? SOURCE_AUTH_OBJECT_ENTRY_LENGTH
194
+ : frameType === SOURCE_AUTH_GROUP_CLOSE_FRAME
195
+ ? SOURCE_AUTH_GROUP_CLOSE_LENGTH
196
+ : 0
197
+ if (expectedLength === 0) throw new Error(`unsupported manifest frame type ${frameType}`)
198
+ if (frame.byteLength !== expectedLength) {
199
+ throw new Error(`manifest frame has invalid length ${frame.byteLength} for type ${frameType}`)
200
+ }
201
+ if (frame[1] !== SOURCE_AUTH_MANIFEST_VERSION) {
202
+ throw new Error(`unsupported manifest version ${frame[1]}`)
203
+ }
204
+ if (frame[2] !== 0 || frame[3] !== 0) throw new Error('manifest reserved bits must be zero')
205
+
206
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
207
+ const groupId = view.getBigUint64(4)
208
+ if (frameType === SOURCE_AUTH_OBJECT_ENTRY_FRAME) {
209
+ const entry: SourceAuthObjectEntry = {
210
+ kind: 'object',
211
+ groupId,
212
+ objectId: view.getUint32(12),
213
+ trueLength: view.getUint32(16),
214
+ digest: frame.slice(20, 52),
215
+ }
216
+ requireTrueLength(entry.trueLength)
217
+ return entry
218
+ }
219
+ const close: SourceAuthGroupClose = {
220
+ kind: 'close',
221
+ groupId,
222
+ objectCount: view.getUint32(12),
223
+ root: frame.slice(16, 48),
224
+ signature: frame.slice(48, 112),
225
+ }
226
+ if (close.objectCount === 0 || close.objectCount > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {
227
+ throw new Error(`objectCount must be between 1 and ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP}`)
228
+ }
229
+ return close
230
+ }
@@ -0,0 +1,307 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import { FecBlockMapper } from '@blockcast/mmt-fec'
3
+ import {
4
+ encodeSourceAuthGroupClose,
5
+ encodeSourceAuthObjectEntry,
6
+ ManifestVerifier,
7
+ sourceAuthMerkleRoot,
8
+ sourceAuthObjectDigest,
9
+ } from './index.js'
10
+
11
+ const mapper = new FecBlockMapper({ interleaveDepthMs: 1000, gopDurationMs: 1000, k: 4 })
12
+
13
+ function createVerifier(options?: {
14
+ mode?: 'off' | 'shadow' | 'enforce'
15
+ onMissingManifest?: 'stall' | 'fallback-shadow'
16
+ signatureValid?: boolean
17
+ bitrate?: number
18
+ authenticated?: boolean
19
+ }) {
20
+ const failures = vi.fn()
21
+ const verifier = new ManifestVerifier({
22
+ manifestAuth: {
23
+ mode: options?.mode ?? 'enforce',
24
+ onMissingManifest: options?.onMissingManifest ?? 'stall',
25
+ },
26
+ verifySignature: () => options?.signatureValid ?? true,
27
+ onFailure: failures,
28
+ })
29
+ verifier.registerTrack({
30
+ broadcastNamespaceId: 'broadcast-1',
31
+ trackName: 'video',
32
+ keyEpoch: 7,
33
+ codec: 'avc1.640033',
34
+ timing: '30/1',
35
+ fecGeometry: 'K=4,T=1200,D=1',
36
+ manifestFormatVersion: 1,
37
+ authScope: new Uint8Array(32).fill(1),
38
+ publicKey: new Uint8Array(32).fill(2),
39
+ bitrate: options?.bitrate ?? 80_000,
40
+ groupDurationMs: 1000,
41
+ mapper,
42
+ manifestChannelAuthenticated: options?.authenticated ?? true,
43
+ })
44
+ return { verifier, failures }
45
+ }
46
+
47
+ function entry(groupId: number, objectId: number, data: Uint8Array) {
48
+ return {
49
+ groupId: BigInt(groupId),
50
+ objectId,
51
+ trueLength: data.byteLength,
52
+ digest: sourceAuthObjectDigest(data),
53
+ }
54
+ }
55
+
56
+ function ingestEntry(verifier: ManifestVerifier, value: ReturnType<typeof entry>): void {
57
+ verifier.ingestManifestFrame('video', encodeSourceAuthObjectEntry(value))
58
+ }
59
+
60
+ function closeGroup(
61
+ verifier: ManifestVerifier,
62
+ groupId: number,
63
+ entries: ReturnType<typeof entry>[],
64
+ ): void {
65
+ verifier.ingestManifestFrame('video', encodeSourceAuthGroupClose({
66
+ groupId: BigInt(groupId),
67
+ objectCount: entries.length,
68
+ root: sourceAuthMerkleRoot(entries),
69
+ signature: new Uint8Array(64).fill(3),
70
+ }))
71
+ }
72
+
73
+ describe('ManifestVerifier', () => {
74
+ test('supports off, shadow, and enforce modes', () => {
75
+ const data = new Uint8Array([1, 2, 3])
76
+ const identity = { trackName: 'video', groupId: 1, objectId: 0, ssId: 4 }
77
+
78
+ const off = createVerifier({ mode: 'off' }).verifier
79
+ const offRelease = vi.fn()
80
+ expect(off.admitObject(identity, data, offRelease)).toMatchObject({ action: 'play' })
81
+ expect(offRelease).toHaveBeenCalledOnce()
82
+
83
+ const shadow = createVerifier({ mode: 'shadow' }).verifier
84
+ const shadowRelease = vi.fn()
85
+ expect(shadow.admitObject(identity, data, shadowRelease)).toMatchObject({
86
+ action: 'play',
87
+ reason: 'manifestAbsent',
88
+ })
89
+ expect(shadowRelease).toHaveBeenCalledOnce()
90
+
91
+ const enforce = createVerifier().verifier
92
+ const enforceRelease = vi.fn()
93
+ expect(enforce.admitObject(identity, data, enforceRelease)).toMatchObject({ action: 'stall' })
94
+ expect(enforceRelease).not.toHaveBeenCalled()
95
+ })
96
+
97
+ test('releases a late matching entry and drops only a digest mismatch', () => {
98
+ const { verifier, failures } = createVerifier()
99
+ const data = new Uint8Array([4, 5, 6])
100
+ const release = vi.fn()
101
+ verifier.admitObject({ trackName: 'video', groupId: 2, objectId: 1, ssId: 9 }, data, release)
102
+ ingestEntry(verifier, entry(2, 1, data))
103
+ expect(release).toHaveBeenCalledOnce()
104
+ expect(verifier.stats.verified).toBe(1)
105
+ expect(failures).toHaveBeenCalledWith(expect.objectContaining({ reason: 'manifestLate' }))
106
+
107
+ const badRelease = vi.fn()
108
+ ingestEntry(verifier, entry(2, 2, new Uint8Array([9])))
109
+ expect(verifier.admitObject(
110
+ { trackName: 'video', groupId: 2, objectId: 2, ssId: 10 },
111
+ new Uint8Array([8]),
112
+ badRelease,
113
+ )).toMatchObject({ action: 'drop', reason: 'digestMismatch' })
114
+ expect(badRelease).not.toHaveBeenCalled()
115
+ })
116
+
117
+ test('verifies one close per group and rejects quarantined media on a bad signature', () => {
118
+ const { verifier } = createVerifier({ authenticated: false, signatureValid: false })
119
+ const data = new Uint8Array([7, 8])
120
+ const manifestEntry = entry(3, 0, data)
121
+ ingestEntry(verifier, manifestEntry)
122
+ const release = vi.fn()
123
+ expect(verifier.admitObject(
124
+ { trackName: 'video', groupId: 3, objectId: 0, ssId: 12 },
125
+ data,
126
+ release,
127
+ ).action).toBe('stall')
128
+ closeGroup(verifier, 3, [manifestEntry])
129
+ expect(release).not.toHaveBeenCalled()
130
+ expect(verifier.stats.failures.sigInvalid).toBe(1)
131
+ })
132
+
133
+ test('fails open on quarantine cap eviction and verifier exceptions', () => {
134
+ const capped = createVerifier({ bitrate: 8 }).verifier
135
+ const capRelease = vi.fn()
136
+ capped.admitObject(
137
+ { trackName: 'video', groupId: 4, objectId: 0, ssId: 16 },
138
+ new Uint8Array(2),
139
+ capRelease,
140
+ )
141
+ expect(capRelease).toHaveBeenCalledOnce()
142
+ expect(capped.stats.failures.verifierError).toBe(1)
143
+
144
+ const release = vi.fn()
145
+ const verifier = new ManifestVerifier({
146
+ manifestAuth: { mode: 'enforce', onMissingManifest: 'stall' },
147
+ verifySignature: () => { throw new Error('WASM unavailable') },
148
+ })
149
+ verifier.registerTrack({
150
+ broadcastNamespaceId: 'broadcast-1', trackName: 'video', keyEpoch: 1,
151
+ codec: 'avc1', timing: '30/1', fecGeometry: 'K=4', manifestFormatVersion: 1,
152
+ authScope: new Uint8Array(32), publicKey: new Uint8Array(32), bitrate: 80_000,
153
+ groupDurationMs: 1000, mapper, manifestChannelAuthenticated: false,
154
+ })
155
+ const data = new Uint8Array([1])
156
+ const manifestEntry = entry(5, 0, data)
157
+ ingestEntry(verifier, manifestEntry)
158
+ verifier.admitObject({ trackName: 'video', groupId: 5, objectId: 0, ssId: 20 }, data, release)
159
+ closeGroup(verifier, 5, [manifestEntry])
160
+ expect(release).toHaveBeenCalledOnce()
161
+ expect(verifier.stats.failures.verifierError).toBe(1)
162
+ })
163
+
164
+ test('switches enforce to shadow live and releases quarantine', () => {
165
+ const { verifier } = createVerifier()
166
+ const release = vi.fn()
167
+ verifier.admitObject(
168
+ { trackName: 'video', groupId: 6, objectId: 0, ssId: 24 },
169
+ new Uint8Array([1]),
170
+ release,
171
+ )
172
+ verifier.setMode('shadow')
173
+ expect(release).toHaveBeenCalledOnce()
174
+ expect(verifier.config.mode).toBe('shadow')
175
+ })
176
+
177
+ test('does not release quarantine for malformed or incomplete manifest frames', () => {
178
+ const { verifier } = createVerifier()
179
+ const release = vi.fn()
180
+ verifier.admitObject(
181
+ { trackName: 'video', groupId: 7, objectId: 0, ssId: 28 },
182
+ new Uint8Array([1]),
183
+ release,
184
+ )
185
+ verifier.ingestManifestFrame('video', new Uint8Array([0xff]))
186
+ verifier.ingestManifestFrame('video', encodeSourceAuthGroupClose({
187
+ groupId: 7n,
188
+ objectCount: 1,
189
+ root: new Uint8Array(32),
190
+ signature: new Uint8Array(64),
191
+ }))
192
+ expect(release).not.toHaveBeenCalled()
193
+ expect(verifier.stats.failures.verifierError).toBe(1)
194
+ expect(verifier.stats.failures.manifestAbsent).toBe(1)
195
+ })
196
+
197
+ test('rejects entries appended after a verified close', () => {
198
+ const { verifier } = createVerifier({ authenticated: false })
199
+ const first = entry(8, 0, new Uint8Array([1]))
200
+ ingestEntry(verifier, first)
201
+ closeGroup(verifier, 8, [first])
202
+
203
+ ingestEntry(verifier, entry(8, 1, new Uint8Array([2])))
204
+ const release = vi.fn()
205
+ expect(verifier.admitObject(
206
+ { trackName: 'video', groupId: 8, objectId: 1, ssId: 33 },
207
+ new Uint8Array([2]),
208
+ release,
209
+ ).action).toBe('stall')
210
+ expect(release).not.toHaveBeenCalled()
211
+ expect(verifier.stats.failures.verifierError).toBe(1)
212
+ })
213
+
214
+ test('requires exact authenticated object length', () => {
215
+ const { verifier } = createVerifier()
216
+ ingestEntry(verifier, entry(9, 0, new Uint8Array([1])))
217
+ const release = vi.fn()
218
+ expect(verifier.admitObject(
219
+ { trackName: 'video', groupId: 9, objectId: 0, ssId: 36 },
220
+ new Uint8Array([1, 2]),
221
+ release,
222
+ )).toMatchObject({ action: 'drop', reason: 'digestMismatch' })
223
+ expect(release).not.toHaveBeenCalled()
224
+ })
225
+
226
+ test('keeps first arrival on duplicate pending object identity', () => {
227
+ const { verifier } = createVerifier()
228
+ const firstRelease = vi.fn()
229
+ const duplicateRelease = vi.fn()
230
+ const identity = { trackName: 'video', groupId: 10, objectId: 0, ssId: 40 }
231
+ verifier.admitObject(identity, new Uint8Array([1]), firstRelease)
232
+ verifier.admitObject(identity, new Uint8Array([1]), duplicateRelease)
233
+ expect(verifier.stats.quarantinedBytes).toBe(1)
234
+ ingestEntry(verifier, entry(10, 0, new Uint8Array([1])))
235
+ expect(firstRelease).toHaveBeenCalledOnce()
236
+ expect(duplicateRelease).not.toHaveBeenCalled()
237
+ })
238
+
239
+ test('re-attempts group close when entries complete after a reordered close frame', () => {
240
+ const { verifier } = createVerifier({ authenticated: false })
241
+ const dataA = new Uint8Array([1, 2])
242
+ const dataB = new Uint8Array([3])
243
+ const entryA = entry(11, 0, dataA)
244
+ const entryB = entry(11, 1, dataB)
245
+
246
+ // Manifest-channel reorder: the close frame beats every object entry.
247
+ closeGroup(verifier, 11, [entryA, entryB])
248
+ expect(verifier.stats.failures.manifestAbsent).toBe(1)
249
+
250
+ const release = vi.fn()
251
+ expect(verifier.admitObject(
252
+ { trackName: 'video', groupId: 11, objectId: 0, ssId: 44 },
253
+ dataA,
254
+ release,
255
+ ).action).toBe('stall')
256
+
257
+ // Before the last entry arrives the object stays quarantined, not played.
258
+ ingestEntry(verifier, entryA)
259
+ expect(release).not.toHaveBeenCalled()
260
+ expect(verifier.stats.verified).toBe(0)
261
+
262
+ // Final entry completes the group: the stashed close is re-attempted and
263
+ // the quarantined object is released verified.
264
+ ingestEntry(verifier, entryB)
265
+ expect(release).toHaveBeenCalledOnce()
266
+ expect(verifier.stats.verified).toBe(1)
267
+
268
+ const directRelease = vi.fn()
269
+ expect(verifier.admitObject(
270
+ { trackName: 'video', groupId: 11, objectId: 1, ssId: 45 },
271
+ dataB,
272
+ directRelease,
273
+ )).toMatchObject({ action: 'play', verified: true })
274
+ expect(directRelease).toHaveBeenCalledOnce()
275
+ })
276
+
277
+ test('verifies a group whose id exceeds Number.MAX_SAFE_INTEGER (u62 group ids)', () => {
278
+ const { verifier, failures } = createVerifier()
279
+ const groupId = 72623859790382856n
280
+ expect(Number.isSafeInteger(Number(groupId))).toBe(false)
281
+
282
+ const data = new Uint8Array([9, 8, 7])
283
+ const bigEntry = {
284
+ groupId,
285
+ objectId: 0,
286
+ trueLength: data.byteLength,
287
+ digest: sourceAuthObjectDigest(data),
288
+ }
289
+ verifier.ingestManifestFrame('video', encodeSourceAuthObjectEntry(bigEntry))
290
+ verifier.ingestManifestFrame('video', encodeSourceAuthGroupClose({
291
+ groupId,
292
+ objectCount: 1,
293
+ root: sourceAuthMerkleRoot([bigEntry]),
294
+ signature: new Uint8Array(64).fill(3),
295
+ }))
296
+
297
+ const release = vi.fn()
298
+ expect(verifier.admitObject(
299
+ { trackName: 'video', groupId, objectId: 0 },
300
+ data,
301
+ release,
302
+ )).toMatchObject({ action: 'play', verified: true })
303
+ expect(release).toHaveBeenCalledOnce()
304
+ expect(verifier.stats.verified).toBe(1)
305
+ expect(failures).not.toHaveBeenCalled()
306
+ })
307
+ })