livekit-client 2.5.7 → 2.5.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +53 -20
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +39 -3
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  11. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +25 -5
  12. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  13. package/dist/src/room/Room.d.ts +3 -1
  14. package/dist/src/room/Room.d.ts.map +1 -1
  15. package/dist/src/room/events.d.ts +5 -1
  16. package/dist/src/room/events.d.ts.map +1 -1
  17. package/dist/src/room/participant/LocalParticipant.d.ts +7 -0
  18. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  19. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +25 -5
  20. package/dist/ts4.2/src/room/Room.d.ts +3 -1
  21. package/dist/ts4.2/src/room/events.d.ts +5 -1
  22. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -0
  23. package/package.json +7 -7
  24. package/src/e2ee/worker/FrameCryptor.test.ts +311 -113
  25. package/src/e2ee/worker/FrameCryptor.ts +10 -5
  26. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +169 -5
  27. package/src/e2ee/worker/ParticipantKeyHandler.ts +50 -20
  28. package/src/e2ee/worker/__snapshots__/ParticipantKeyHandler.test.ts.snap +356 -0
  29. package/src/room/Room.ts +8 -0
  30. package/src/room/events.ts +5 -0
  31. package/src/room/participant/LocalParticipant.ts +26 -1
  32. package/src/room/track/LocalTrackPublication.ts +1 -1
@@ -1,5 +1,6 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { KEY_PROVIDER_DEFAULTS } from '../constants';
1
+ import { describe, expect, it, vitest } from 'vitest';
2
+ import { ENCRYPTION_ALGORITHM, KEY_PROVIDER_DEFAULTS } from '../constants';
3
+ import { KeyHandlerEvent } from '../events';
3
4
  import { createKeyMaterialFromString } from '../utils';
4
5
  import { ParticipantKeyHandler } from './ParticipantKeyHandler';
5
6
 
@@ -35,11 +36,38 @@ describe('ParticipantKeyHandler', () => {
35
36
  expect(keyHandler.getKeySet(0)?.material).toEqual(materialB);
36
37
  });
37
38
 
38
- it('marks invalid if more than failureTolerance failures', async () => {
39
+ it('defaults to key index of 0 when setting key', async () => {
40
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
41
+ ...KEY_PROVIDER_DEFAULTS,
42
+ });
43
+
44
+ const materialA = await createKeyMaterialFromString('passwordA');
45
+
46
+ await keyHandler.setKey(materialA);
47
+
48
+ expect(keyHandler.getKeySet(0)?.material).toEqual(materialA);
49
+ });
50
+
51
+ it('defaults to current key index when getting key', async () => {
52
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
53
+ ...KEY_PROVIDER_DEFAULTS,
54
+ });
55
+
56
+ const materialA = await createKeyMaterialFromString('passwordA');
57
+
58
+ await keyHandler.setKey(materialA, 10);
59
+
60
+ expect(keyHandler.getKeySet()?.material).toEqual(materialA);
61
+ });
62
+
63
+ it('marks current key invalid if more than failureTolerance failures', async () => {
39
64
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
40
65
  ...KEY_PROVIDER_DEFAULTS,
41
66
  failureTolerance: 2,
42
67
  });
68
+
69
+ keyHandler.setCurrentKeyIndex(10);
70
+
43
71
  expect(keyHandler.hasValidKey).toBe(true);
44
72
 
45
73
  // 1
@@ -55,13 +83,16 @@ describe('ParticipantKeyHandler', () => {
55
83
  expect(keyHandler.hasValidKey).toBe(false);
56
84
  });
57
85
 
58
- it('marks valid on encryption success', async () => {
86
+ it('marks current key valid on encryption success', async () => {
59
87
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
60
88
  ...KEY_PROVIDER_DEFAULTS,
61
89
  failureTolerance: 0,
62
90
  });
63
91
 
92
+ keyHandler.setCurrentKeyIndex(10);
93
+
64
94
  expect(keyHandler.hasValidKey).toBe(true);
95
+ expect(keyHandler.hasInvalidKeyAtIndex(0)).toBe(false);
65
96
 
66
97
  keyHandler.decryptionFailure();
67
98
 
@@ -72,13 +103,62 @@ describe('ParticipantKeyHandler', () => {
72
103
  expect(keyHandler.hasValidKey).toBe(true);
73
104
  });
74
105
 
106
+ it('marks specific key invalid if more than failureTolerance failures', async () => {
107
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
108
+ ...KEY_PROVIDER_DEFAULTS,
109
+ failureTolerance: 2,
110
+ });
111
+
112
+ // set the current key to something different from what we are testing
113
+ keyHandler.setCurrentKeyIndex(10);
114
+
115
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
116
+
117
+ // 1
118
+ keyHandler.decryptionFailure(5);
119
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
120
+
121
+ // 2
122
+ keyHandler.decryptionFailure(5);
123
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
124
+
125
+ // 3
126
+ keyHandler.decryptionFailure(5);
127
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(true);
128
+
129
+ expect(keyHandler.hasInvalidKeyAtIndex(10)).toBe(false);
130
+ });
131
+
132
+ it('marks specific key valid on encryption success', async () => {
133
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
134
+ ...KEY_PROVIDER_DEFAULTS,
135
+ failureTolerance: 0,
136
+ });
137
+
138
+ // set the current key to something different from what we are testing
139
+ keyHandler.setCurrentKeyIndex(10);
140
+
141
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
142
+
143
+ keyHandler.decryptionFailure(5);
144
+
145
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(true);
146
+
147
+ keyHandler.decryptionSuccess(5);
148
+
149
+ expect(keyHandler.hasInvalidKeyAtIndex(5)).toBe(false);
150
+ });
151
+
75
152
  it('marks valid on new key', async () => {
76
153
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
77
154
  ...KEY_PROVIDER_DEFAULTS,
78
155
  failureTolerance: 0,
79
156
  });
80
157
 
158
+ keyHandler.setCurrentKeyIndex(10);
159
+
81
160
  expect(keyHandler.hasValidKey).toBe(true);
161
+ expect(keyHandler.hasInvalidKeyAtIndex(0)).toBe(false);
82
162
 
83
163
  keyHandler.decryptionFailure();
84
164
 
@@ -108,7 +188,14 @@ describe('ParticipantKeyHandler', () => {
108
188
  expect(keyHandler.getCurrentKeyIndex()).toBe(10);
109
189
  });
110
190
 
111
- it('allows many failures if failureTolerance is -1', async () => {
191
+ it('allows currentKeyIndex to be explicitly set', async () => {
192
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
193
+
194
+ keyHandler.setCurrentKeyIndex(10);
195
+ expect(keyHandler.getCurrentKeyIndex()).toBe(10);
196
+ });
197
+
198
+ it('allows many failures if failureTolerance is less than zero', async () => {
112
199
  const keyHandler = new ParticipantKeyHandler(participantIdentity, {
113
200
  ...KEY_PROVIDER_DEFAULTS,
114
201
  failureTolerance: -1,
@@ -119,4 +206,81 @@ describe('ParticipantKeyHandler', () => {
119
206
  expect(keyHandler.hasValidKey).toBe(true);
120
207
  }
121
208
  });
209
+
210
+ describe('resetKeyStatus', () => {
211
+ it('marks all keys as valid if no index is provided', () => {
212
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, {
213
+ ...KEY_PROVIDER_DEFAULTS,
214
+ failureTolerance: 0,
215
+ });
216
+
217
+ for (let i = 0; i < KEY_PROVIDER_DEFAULTS.keyringSize; i++) {
218
+ keyHandler.decryptionFailure(i);
219
+ expect(keyHandler.hasInvalidKeyAtIndex(i)).toBe(true);
220
+ }
221
+
222
+ keyHandler.resetKeyStatus();
223
+
224
+ for (let i = 0; i < KEY_PROVIDER_DEFAULTS.keyringSize; i++) {
225
+ expect(keyHandler.hasInvalidKeyAtIndex(i)).toBe(false);
226
+ }
227
+ });
228
+ });
229
+
230
+ describe('ratchetKey', () => {
231
+ it('emits event', async () => {
232
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
233
+
234
+ const material = await createKeyMaterialFromString('password');
235
+
236
+ const keyRatched = vitest.fn();
237
+
238
+ keyHandler.on(KeyHandlerEvent.KeyRatcheted, keyRatched);
239
+
240
+ await keyHandler.setKey(material);
241
+
242
+ await keyHandler.ratchetKey();
243
+
244
+ const newMaterial = keyHandler.getKeySet()?.material;
245
+
246
+ expect(keyRatched).toHaveBeenCalledWith(newMaterial, participantIdentity, 0);
247
+ });
248
+
249
+ it('ratchets keys predictably', async () => {
250
+ // we can't extract the keys directly, so we instead use them to encrypt a known plaintext
251
+ const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
252
+
253
+ const originalMaterial = await createKeyMaterialFromString('password');
254
+
255
+ await keyHandler.setKey(originalMaterial);
256
+
257
+ const ciphertexts: Uint8Array[] = [];
258
+
259
+ const plaintext = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
260
+
261
+ const iv = new Uint8Array(12);
262
+ const additionalData = new Uint8Array(0);
263
+
264
+ for (let i = 0; i < 10; i++) {
265
+ const { encryptionKey } = keyHandler.getKeySet()!;
266
+
267
+ const ciphertext = await crypto.subtle.encrypt(
268
+ {
269
+ name: ENCRYPTION_ALGORITHM,
270
+ iv,
271
+ additionalData,
272
+ },
273
+ encryptionKey,
274
+ plaintext,
275
+ );
276
+ ciphertexts.push(new Uint8Array(ciphertext));
277
+ await keyHandler.ratchetKey();
278
+ }
279
+ // check that all ciphertexts are unique
280
+ expect(new Set(ciphertexts.map((x) => new TextDecoder().decode(x))).size).toEqual(
281
+ ciphertexts.length,
282
+ );
283
+ expect(ciphertexts).matchSnapshot('ciphertexts');
284
+ });
285
+ });
122
286
  });
@@ -21,18 +21,19 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
21
21
 
22
22
  private cryptoKeyRing: Array<KeySet | undefined>;
23
23
 
24
+ private decryptionFailureCounts: Array<number>;
25
+
24
26
  private keyProviderOptions: KeyProviderOptions;
25
27
 
26
28
  private ratchetPromiseMap: Map<number, Promise<CryptoKey>>;
27
29
 
28
30
  private participantIdentity: string;
29
31
 
30
- private decryptionFailureCount = 0;
31
-
32
- private _hasValidKey: boolean = true;
33
-
34
- get hasValidKey() {
35
- return this._hasValidKey;
32
+ /**
33
+ * true if the current key has not been marked as invalid
34
+ */
35
+ get hasValidKey(): boolean {
36
+ return !this.hasInvalidKeyAtIndex(this.currentKeyIndex);
36
37
  }
37
38
 
38
39
  constructor(participantIdentity: string, keyProviderOptions: KeyProviderOptions) {
@@ -42,35 +43,64 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
42
43
  throw new TypeError('Keyring size needs to be between 1 and 256');
43
44
  }
44
45
  this.cryptoKeyRing = new Array(keyProviderOptions.keyringSize).fill(undefined);
46
+ this.decryptionFailureCounts = new Array(keyProviderOptions.keyringSize).fill(0);
45
47
  this.keyProviderOptions = keyProviderOptions;
46
48
  this.ratchetPromiseMap = new Map();
47
49
  this.participantIdentity = participantIdentity;
48
- this.resetKeyStatus();
49
50
  }
50
51
 
51
- decryptionFailure() {
52
+ /**
53
+ * Returns true if the key at the given index is marked as invalid.
54
+ *
55
+ * @param keyIndex the index of the key
56
+ */
57
+ hasInvalidKeyAtIndex(keyIndex: number): boolean {
58
+ return (
59
+ this.keyProviderOptions.failureTolerance >= 0 &&
60
+ this.decryptionFailureCounts[keyIndex] > this.keyProviderOptions.failureTolerance
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Informs the key handler that a decryption failure occurred for an encryption key.
66
+ * @internal
67
+ * @param keyIndex the key index for which the failure occurred. Defaults to the current key index.
68
+ */
69
+ decryptionFailure(keyIndex: number = this.currentKeyIndex): void {
52
70
  if (this.keyProviderOptions.failureTolerance < 0) {
53
71
  return;
54
72
  }
55
- this.decryptionFailureCount += 1;
56
73
 
57
- if (this.decryptionFailureCount > this.keyProviderOptions.failureTolerance) {
58
- workerLogger.warn(`key for ${this.participantIdentity} is being marked as invalid`);
59
- this._hasValidKey = false;
74
+ this.decryptionFailureCounts[keyIndex] += 1;
75
+
76
+ if (this.decryptionFailureCounts[keyIndex] > this.keyProviderOptions.failureTolerance) {
77
+ workerLogger.warn(
78
+ `key for ${this.participantIdentity} at index ${keyIndex} is being marked as invalid`,
79
+ );
60
80
  }
61
81
  }
62
82
 
63
- decryptionSuccess() {
64
- this.resetKeyStatus();
83
+ /**
84
+ * Informs the key handler that a frame was successfully decrypted using an encryption key.
85
+ * @internal
86
+ * @param keyIndex the key index for which the success occurred. Defaults to the current key index.
87
+ */
88
+ decryptionSuccess(keyIndex: number = this.currentKeyIndex): void {
89
+ this.resetKeyStatus(keyIndex);
65
90
  }
66
91
 
67
92
  /**
68
93
  * Call this after user initiated ratchet or a new key has been set in order to make sure to mark potentially
69
94
  * invalid keys as valid again
95
+ *
96
+ * @param keyIndex the index of the key. Defaults to the current key index.
70
97
  */
71
- resetKeyStatus() {
72
- this.decryptionFailureCount = 0;
73
- this._hasValidKey = true;
98
+ resetKeyStatus(keyIndex?: number): void {
99
+ if (keyIndex === undefined) {
100
+ this.decryptionFailureCounts.fill(0);
101
+ } else {
102
+ this.decryptionFailureCounts[keyIndex] = 0;
103
+ }
74
104
  }
75
105
 
76
106
  /**
@@ -103,7 +133,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
103
133
  );
104
134
 
105
135
  if (setKey) {
106
- this.setKeyFromMaterial(newMaterial, currentKeyIndex, true);
136
+ await this.setKeyFromMaterial(newMaterial, currentKeyIndex, true);
107
137
  this.emit(
108
138
  KeyHandlerEvent.KeyRatcheted,
109
139
  newMaterial,
@@ -130,7 +160,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
130
160
  */
131
161
  async setKey(material: CryptoKey, keyIndex = 0) {
132
162
  await this.setKeyFromMaterial(material, keyIndex);
133
- this.resetKeyStatus();
163
+ this.resetKeyStatus(keyIndex);
134
164
  }
135
165
 
136
166
  /**
@@ -161,7 +191,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
161
191
 
162
192
  async setCurrentKeyIndex(index: number) {
163
193
  this.currentKeyIndex = index % this.cryptoKeyRing.length;
164
- this.resetKeyStatus();
194
+ this.resetKeyStatus(index);
165
195
  }
166
196
 
167
197
  getCurrentKeyIndex() {
@@ -0,0 +1,356 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`ParticipantKeyHandler > ratchetKey > ratchets keys predictably > ciphertexts 1`] = `
4
+ [
5
+ Uint8Array [
6
+ 42,
7
+ 226,
8
+ 94,
9
+ 49,
10
+ 152,
11
+ 18,
12
+ 79,
13
+ 1,
14
+ 55,
15
+ 190,
16
+ 250,
17
+ 80,
18
+ 143,
19
+ 19,
20
+ 134,
21
+ 218,
22
+ 200,
23
+ 55,
24
+ 87,
25
+ 102,
26
+ 117,
27
+ 217,
28
+ 130,
29
+ 48,
30
+ 11,
31
+ 66,
32
+ 63,
33
+ 102,
34
+ 115,
35
+ 144,
36
+ 117,
37
+ 92,
38
+ 232,
39
+ ],
40
+ Uint8Array [
41
+ 4,
42
+ 164,
43
+ 120,
44
+ 76,
45
+ 172,
46
+ 225,
47
+ 17,
48
+ 14,
49
+ 176,
50
+ 186,
51
+ 111,
52
+ 5,
53
+ 10,
54
+ 176,
55
+ 29,
56
+ 42,
57
+ 19,
58
+ 215,
59
+ 72,
60
+ 227,
61
+ 203,
62
+ 139,
63
+ 219,
64
+ 147,
65
+ 85,
66
+ 78,
67
+ 62,
68
+ 191,
69
+ 186,
70
+ 123,
71
+ 248,
72
+ 60,
73
+ 147,
74
+ ],
75
+ Uint8Array [
76
+ 58,
77
+ 173,
78
+ 85,
79
+ 162,
80
+ 211,
81
+ 200,
82
+ 200,
83
+ 87,
84
+ 214,
85
+ 65,
86
+ 218,
87
+ 92,
88
+ 74,
89
+ 53,
90
+ 143,
91
+ 183,
92
+ 58,
93
+ 242,
94
+ 19,
95
+ 39,
96
+ 226,
97
+ 180,
98
+ 204,
99
+ 127,
100
+ 109,
101
+ 219,
102
+ 156,
103
+ 152,
104
+ 103,
105
+ 158,
106
+ 186,
107
+ 240,
108
+ 161,
109
+ ],
110
+ Uint8Array [
111
+ 46,
112
+ 137,
113
+ 107,
114
+ 8,
115
+ 239,
116
+ 248,
117
+ 217,
118
+ 214,
119
+ 106,
120
+ 234,
121
+ 103,
122
+ 34,
123
+ 108,
124
+ 179,
125
+ 18,
126
+ 186,
127
+ 5,
128
+ 33,
129
+ 31,
130
+ 21,
131
+ 9,
132
+ 48,
133
+ 194,
134
+ 205,
135
+ 206,
136
+ 136,
137
+ 6,
138
+ 179,
139
+ 64,
140
+ 150,
141
+ 126,
142
+ 175,
143
+ 132,
144
+ ],
145
+ Uint8Array [
146
+ 249,
147
+ 123,
148
+ 86,
149
+ 179,
150
+ 18,
151
+ 9,
152
+ 149,
153
+ 42,
154
+ 110,
155
+ 112,
156
+ 29,
157
+ 193,
158
+ 208,
159
+ 63,
160
+ 48,
161
+ 118,
162
+ 15,
163
+ 186,
164
+ 27,
165
+ 101,
166
+ 23,
167
+ 31,
168
+ 111,
169
+ 152,
170
+ 193,
171
+ 235,
172
+ 89,
173
+ 25,
174
+ 161,
175
+ 246,
176
+ 231,
177
+ 198,
178
+ 126,
179
+ ],
180
+ Uint8Array [
181
+ 56,
182
+ 134,
183
+ 196,
184
+ 195,
185
+ 242,
186
+ 10,
187
+ 187,
188
+ 122,
189
+ 111,
190
+ 179,
191
+ 147,
192
+ 206,
193
+ 74,
194
+ 153,
195
+ 45,
196
+ 244,
197
+ 88,
198
+ 119,
199
+ 25,
200
+ 114,
201
+ 11,
202
+ 2,
203
+ 149,
204
+ 121,
205
+ 227,
206
+ 219,
207
+ 39,
208
+ 11,
209
+ 13,
210
+ 175,
211
+ 182,
212
+ 168,
213
+ 96,
214
+ ],
215
+ Uint8Array [
216
+ 36,
217
+ 12,
218
+ 119,
219
+ 157,
220
+ 146,
221
+ 42,
222
+ 43,
223
+ 107,
224
+ 61,
225
+ 43,
226
+ 242,
227
+ 166,
228
+ 11,
229
+ 180,
230
+ 148,
231
+ 39,
232
+ 177,
233
+ 248,
234
+ 194,
235
+ 166,
236
+ 201,
237
+ 53,
238
+ 192,
239
+ 57,
240
+ 62,
241
+ 70,
242
+ 37,
243
+ 252,
244
+ 243,
245
+ 156,
246
+ 140,
247
+ 185,
248
+ 42,
249
+ ],
250
+ Uint8Array [
251
+ 222,
252
+ 127,
253
+ 32,
254
+ 228,
255
+ 99,
256
+ 198,
257
+ 31,
258
+ 158,
259
+ 192,
260
+ 101,
261
+ 82,
262
+ 129,
263
+ 175,
264
+ 153,
265
+ 112,
266
+ 43,
267
+ 57,
268
+ 207,
269
+ 246,
270
+ 54,
271
+ 127,
272
+ 165,
273
+ 99,
274
+ 254,
275
+ 4,
276
+ 178,
277
+ 40,
278
+ 53,
279
+ 66,
280
+ 92,
281
+ 13,
282
+ 69,
283
+ 0,
284
+ ],
285
+ Uint8Array [
286
+ 249,
287
+ 181,
288
+ 181,
289
+ 168,
290
+ 27,
291
+ 48,
292
+ 221,
293
+ 221,
294
+ 174,
295
+ 187,
296
+ 16,
297
+ 153,
298
+ 144,
299
+ 156,
300
+ 164,
301
+ 232,
302
+ 185,
303
+ 202,
304
+ 132,
305
+ 58,
306
+ 75,
307
+ 156,
308
+ 232,
309
+ 238,
310
+ 191,
311
+ 33,
312
+ 9,
313
+ 31,
314
+ 151,
315
+ 43,
316
+ 224,
317
+ 136,
318
+ 190,
319
+ ],
320
+ Uint8Array [
321
+ 58,
322
+ 99,
323
+ 16,
324
+ 151,
325
+ 148,
326
+ 131,
327
+ 138,
328
+ 94,
329
+ 230,
330
+ 95,
331
+ 38,
332
+ 81,
333
+ 137,
334
+ 162,
335
+ 78,
336
+ 61,
337
+ 243,
338
+ 207,
339
+ 255,
340
+ 109,
341
+ 48,
342
+ 248,
343
+ 217,
344
+ 135,
345
+ 133,
346
+ 248,
347
+ 56,
348
+ 74,
349
+ 41,
350
+ 113,
351
+ 153,
352
+ 108,
353
+ 202,
354
+ ],
355
+ ]
356
+ `;