@xiboplayer/utils 0.7.0 → 0.7.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/utils",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Shared utilities for Xibo Player packages",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -12,7 +12,7 @@
12
12
  "./config": "./src/config.js"
13
13
  },
14
14
  "dependencies": {
15
- "@xiboplayer/crypto": "0.7.0"
15
+ "@xiboplayer/crypto": "0.7.2"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0"
package/src/config.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * In browser (PWA player): localStorage is primary, env vars override if set.
14
14
  */
15
15
  import { generateRsaKeyPair, isValidPemKey } from '@xiboplayer/crypto';
16
+ import { openIDB } from './idb.js';
16
17
 
17
18
  const GLOBAL_KEY = 'xibo_global'; // Device identity (all CMSes)
18
19
  const CMS_PREFIX = 'xibo_cms:'; // Per-CMS config prefix
@@ -351,26 +352,17 @@ export class Config {
351
352
  * IndexedDB survives "Clear site data" in some browsers where localStorage doesn't.
352
353
  * @param {Object} keys - Key-value pairs to store (e.g. { hardwareKey: '...', xmrPubKey: '...' })
353
354
  */
354
- _backupKeys(keys) {
355
+ async _backupKeys(keys) {
355
356
  try {
356
- const req = indexedDB.open(HW_DB_NAME, HW_DB_VERSION);
357
- req.onupgradeneeded = () => {
358
- const db = req.result;
359
- if (!db.objectStoreNames.contains('keys')) {
360
- db.createObjectStore('keys');
361
- }
362
- };
363
- req.onsuccess = () => {
364
- const db = req.result;
365
- const tx = db.transaction('keys', 'readwrite');
366
- const store = tx.objectStore('keys');
367
- for (const [k, v] of Object.entries(keys)) {
368
- store.put(v, k);
369
- }
370
- tx.oncomplete = () => {
371
- console.log('[Config] Keys backed up to IndexedDB:', Object.keys(keys).join(', '));
372
- db.close();
373
- };
357
+ const db = await openIDB(HW_DB_NAME, HW_DB_VERSION, 'keys');
358
+ const tx = db.transaction('keys', 'readwrite');
359
+ const store = tx.objectStore('keys');
360
+ for (const [k, v] of Object.entries(keys)) {
361
+ store.put(v, k);
362
+ }
363
+ tx.oncomplete = () => {
364
+ console.log('[Config] Keys backed up to IndexedDB:', Object.keys(keys).join(', '));
365
+ db.close();
374
366
  };
375
367
  } catch (e) {
376
368
  // IndexedDB not available — localStorage-only mode
@@ -390,19 +382,8 @@ export class Config {
390
382
  * differs from the current one, it restores the original key.
391
383
  */
392
384
  async _restoreHardwareKeyFromBackup() {
393
- if (typeof indexedDB === 'undefined') return;
394
385
  try {
395
- const db = await new Promise((resolve, reject) => {
396
- const req = indexedDB.open(HW_DB_NAME, HW_DB_VERSION);
397
- req.onupgradeneeded = () => {
398
- const db = req.result;
399
- if (!db.objectStoreNames.contains('keys')) {
400
- db.createObjectStore('keys');
401
- }
402
- };
403
- req.onsuccess = () => resolve(req.result);
404
- req.onerror = () => reject(req.error);
405
- });
386
+ const db = await openIDB(HW_DB_NAME, HW_DB_VERSION, 'keys');
406
387
 
407
388
  const tx = db.transaction('keys', 'readonly');
408
389
  const store = tx.objectStore('keys');
@@ -450,32 +431,6 @@ export class Config {
450
431
  return hardwareKey;
451
432
  }
452
433
 
453
- getCanvasFingerprint() {
454
- // Generate stable canvas fingerprint (same for same GPU/driver)
455
- try {
456
- const canvas = document.createElement('canvas');
457
- const ctx = canvas.getContext('2d');
458
- if (!ctx) return 'no-canvas';
459
-
460
- // Draw test pattern (same rendering = same device)
461
- ctx.textBaseline = 'top';
462
- ctx.font = '14px Arial';
463
- ctx.fillStyle = '#f60';
464
- ctx.fillRect(125, 1, 62, 20);
465
- ctx.fillStyle = '#069';
466
- ctx.fillText('Xibo Player', 2, 15);
467
-
468
- return canvas.toDataURL();
469
- } catch (e) {
470
- return 'canvas-error';
471
- }
472
- }
473
-
474
- generateHardwareKey() {
475
- // For backwards compatibility
476
- return this.generateStableHardwareKey();
477
- }
478
-
479
434
  generateXmrChannel() {
480
435
  // Generate UUID for XMR channel
481
436
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
@@ -224,61 +224,6 @@ describe('Config', () => {
224
224
  });
225
225
  });
226
226
 
227
- describe('Canvas Fingerprint', () => {
228
- let createElementSpy;
229
-
230
- beforeEach(() => {
231
- config = new Config();
232
-
233
- // Mock canvas via spying on document.createElement
234
- const mockCanvas = {
235
- getContext: vi.fn(() => ({
236
- textBaseline: '',
237
- font: '',
238
- fillStyle: '',
239
- fillRect: vi.fn(),
240
- fillText: vi.fn()
241
- })),
242
- toDataURL: vi.fn(() => 'data:image/png;base64,mockdata')
243
- };
244
-
245
- createElementSpy = vi.spyOn(document, 'createElement').mockReturnValue(mockCanvas);
246
- });
247
-
248
- afterEach(() => {
249
- createElementSpy.mockRestore();
250
- });
251
-
252
- it('should generate canvas fingerprint', () => {
253
- const fingerprint = config.getCanvasFingerprint();
254
-
255
- expect(fingerprint).toBe('data:image/png;base64,mockdata');
256
- expect(createElementSpy).toHaveBeenCalledWith('canvas');
257
- });
258
-
259
- it('should return "no-canvas" when canvas context unavailable', () => {
260
- const mockCanvas = {
261
- getContext: vi.fn(() => null)
262
- };
263
-
264
- createElementSpy.mockReturnValue(mockCanvas);
265
-
266
- const fingerprint = config.getCanvasFingerprint();
267
-
268
- expect(fingerprint).toBe('no-canvas');
269
- });
270
-
271
- it('should return "canvas-error" on exception', () => {
272
- createElementSpy.mockImplementation(() => {
273
- throw new Error('Canvas not supported');
274
- });
275
-
276
- const fingerprint = config.getCanvasFingerprint();
277
-
278
- expect(fingerprint).toBe('canvas-error');
279
- });
280
- });
281
-
282
227
  describe('Configuration Getters/Setters', () => {
283
228
  beforeEach(() => {
284
229
  config = new Config();
@@ -391,20 +336,6 @@ describe('Config', () => {
391
336
  });
392
337
  });
393
338
 
394
- describe('Backwards Compatibility', () => {
395
- beforeEach(() => {
396
- config = new Config();
397
- });
398
-
399
- it('should support generateHardwareKey() alias', () => {
400
- const key1 = config.generateHardwareKey();
401
- const key2 = config.generateStableHardwareKey();
402
-
403
- // Both should generate valid keys
404
- expect(key1).toMatch(/^pwa-[0-9a-f]{28}$/);
405
- expect(key2).toMatch(/^pwa-[0-9a-f]{28}$/);
406
- });
407
- });
408
339
 
409
340
  describe('Edge Cases', () => {
410
341
  it('should handle missing hardwareKey in loaded config', () => {
package/src/idb.js ADDED
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * Shared IndexedDB open helper — avoids duplicating the open/upgrade
5
+ * boilerplate across stats, core, and config packages.
6
+ *
7
+ * @param {string} dbName - Database name
8
+ * @param {number} version - Schema version
9
+ * @param {string} storeName - Object store to create on upgrade
10
+ * @param {Object} [options]
11
+ * @param {string} [options.keyPath] - Key path for the store (auto-increment if set)
12
+ * @param {string} [options.indexName] - Index to create on the store
13
+ * @param {string} [options.indexKey] - Key for the index
14
+ * @returns {Promise<IDBDatabase>}
15
+ */
16
+ export function openIDB(dbName, version, storeName, options = {}) {
17
+ if (typeof indexedDB === 'undefined') {
18
+ return Promise.reject(new Error('IndexedDB not available'));
19
+ }
20
+ return new Promise((resolve, reject) => {
21
+ const req = indexedDB.open(dbName, version);
22
+ req.onupgradeneeded = () => {
23
+ const db = req.result;
24
+ if (!db.objectStoreNames.contains(storeName)) {
25
+ const storeOpts = options.keyPath
26
+ ? { keyPath: options.keyPath, autoIncrement: true }
27
+ : undefined;
28
+ const store = db.createObjectStore(storeName, storeOpts);
29
+ if (options.indexName && options.indexKey) {
30
+ store.createIndex(options.indexName, options.indexKey, { unique: false });
31
+ }
32
+ }
33
+ };
34
+ req.onsuccess = () => resolve(req.result);
35
+ req.onerror = () => reject(req.error);
36
+ });
37
+ }
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ export { EventEmitter } from './event-emitter.js';
8
8
  import { config as _config } from './config.js';
9
9
  export { config, SHELL_ONLY_KEYS, extractPwaConfig, computeCmsId, fnvHash, warnPlatformMismatch } from './config.js';
10
10
  export { fetchWithRetry } from './fetch-retry.js';
11
+ export { openIDB } from './idb.js';
11
12
  export { CmsApiClient, CmsApiError } from './cms-api.js';
12
13
 
13
14
  /**