@xiboplayer/utils 0.3.1 → 0.3.3
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 +4 -2
- package/src/config.js +43 -4
- package/src/config.test.js +108 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/utils",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Shared utilities for Xibo Player packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"./event-emitter": "./src/event-emitter.js",
|
|
11
11
|
"./config": "./src/config.js"
|
|
12
12
|
},
|
|
13
|
-
"dependencies": {
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@xiboplayer/crypto": "0.3.3"
|
|
15
|
+
},
|
|
14
16
|
"devDependencies": {
|
|
15
17
|
"vitest": "^2.0.0"
|
|
16
18
|
},
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* In Node.js (tests, CLI): environment variables are the only source.
|
|
5
5
|
* In browser (PWA player): localStorage is primary, env vars override if set.
|
|
6
6
|
*/
|
|
7
|
+
import { generateRsaKeyPair, isValidPemKey } from '@xiboplayer/crypto';
|
|
7
8
|
|
|
8
9
|
const STORAGE_KEY = 'xibo_config';
|
|
9
10
|
const HW_DB_NAME = 'xibo-hw-backup';
|
|
@@ -99,10 +100,11 @@ export class Config {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
|
-
* Backup
|
|
103
|
+
* Backup keys to IndexedDB (more persistent than localStorage).
|
|
103
104
|
* IndexedDB survives "Clear site data" in some browsers where localStorage doesn't.
|
|
105
|
+
* @param {Object} keys - Key-value pairs to store (e.g. { hardwareKey: '...', xmrPubKey: '...' })
|
|
104
106
|
*/
|
|
105
|
-
|
|
107
|
+
_backupKeys(keys) {
|
|
106
108
|
try {
|
|
107
109
|
const req = indexedDB.open(HW_DB_NAME, HW_DB_VERSION);
|
|
108
110
|
req.onupgradeneeded = () => {
|
|
@@ -114,9 +116,12 @@ export class Config {
|
|
|
114
116
|
req.onsuccess = () => {
|
|
115
117
|
const db = req.result;
|
|
116
118
|
const tx = db.transaction('keys', 'readwrite');
|
|
117
|
-
tx.objectStore('keys')
|
|
119
|
+
const store = tx.objectStore('keys');
|
|
120
|
+
for (const [k, v] of Object.entries(keys)) {
|
|
121
|
+
store.put(v, k);
|
|
122
|
+
}
|
|
118
123
|
tx.oncomplete = () => {
|
|
119
|
-
console.log('[Config]
|
|
124
|
+
console.log('[Config] Keys backed up to IndexedDB:', Object.keys(keys).join(', '));
|
|
120
125
|
db.close();
|
|
121
126
|
};
|
|
122
127
|
};
|
|
@@ -125,6 +130,13 @@ export class Config {
|
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Backup hardware key to IndexedDB (convenience wrapper).
|
|
135
|
+
*/
|
|
136
|
+
_backupHardwareKey(key) {
|
|
137
|
+
this._backupKeys({ hardwareKey: key });
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
/**
|
|
129
141
|
* Restore hardware key from IndexedDB if localStorage was cleared.
|
|
130
142
|
* Runs async after construction — if a backed-up key is found and
|
|
@@ -236,6 +248,31 @@ export class Config {
|
|
|
236
248
|
});
|
|
237
249
|
}
|
|
238
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Ensure an RSA key pair exists for XMR registration.
|
|
253
|
+
* If keys are missing or invalid, generates a new pair and persists them.
|
|
254
|
+
* Idempotent — safe to call multiple times.
|
|
255
|
+
*/
|
|
256
|
+
async ensureXmrKeyPair() {
|
|
257
|
+
if (this.data.xmrPubKey && isValidPemKey(this.data.xmrPubKey)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log('[Config] Generating RSA key pair for XMR registration...');
|
|
262
|
+
const { publicKeyPem, privateKeyPem } = await generateRsaKeyPair();
|
|
263
|
+
|
|
264
|
+
this.data.xmrPubKey = publicKeyPem;
|
|
265
|
+
this.data.xmrPrivKey = privateKeyPem;
|
|
266
|
+
this.save();
|
|
267
|
+
|
|
268
|
+
// Backup RSA keys to IndexedDB alongside hardware key
|
|
269
|
+
if (typeof indexedDB !== 'undefined') {
|
|
270
|
+
this._backupKeys({ xmrPubKey: publicKeyPem, xmrPrivKey: privateKeyPem });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log('[Config] RSA key pair generated and saved');
|
|
274
|
+
}
|
|
275
|
+
|
|
239
276
|
hash(str) {
|
|
240
277
|
// FNV-1a hash algorithm (better distribution than simple hash)
|
|
241
278
|
// Produces high-entropy 32-character hex string
|
|
@@ -283,6 +320,8 @@ export class Config {
|
|
|
283
320
|
return this.data.hardwareKey;
|
|
284
321
|
}
|
|
285
322
|
get xmrChannel() { return this.data.xmrChannel; }
|
|
323
|
+
get xmrPubKey() { return this.data.xmrPubKey || ''; }
|
|
324
|
+
get xmrPrivKey() { return this.data.xmrPrivKey || ''; }
|
|
286
325
|
}
|
|
287
326
|
|
|
288
327
|
export const config = new Config();
|
package/src/config.test.js
CHANGED
|
@@ -5,6 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// Mock @xiboplayer/crypto before importing Config
|
|
10
|
+
vi.mock('@xiboplayer/crypto', () => {
|
|
11
|
+
let callCount = 0;
|
|
12
|
+
return {
|
|
13
|
+
generateRsaKeyPair: vi.fn(async () => {
|
|
14
|
+
callCount++;
|
|
15
|
+
return {
|
|
16
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKEY${callCount}==\n-----END PUBLIC KEY-----`,
|
|
17
|
+
privateKeyPem: `-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFPRIVKEY${callCount}==\n-----END PRIVATE KEY-----`,
|
|
18
|
+
};
|
|
19
|
+
}),
|
|
20
|
+
isValidPemKey: vi.fn((pem) => {
|
|
21
|
+
if (!pem || typeof pem !== 'string') return false;
|
|
22
|
+
return /^-----BEGIN (PUBLIC KEY|PRIVATE KEY)-----\n/.test(pem);
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
8
27
|
import { Config } from './config.js';
|
|
9
28
|
|
|
10
29
|
describe('Config', () => {
|
|
@@ -470,4 +489,93 @@ describe('Config', () => {
|
|
|
470
489
|
expect(config2.displayName).toBe('Persist Display');
|
|
471
490
|
});
|
|
472
491
|
});
|
|
492
|
+
|
|
493
|
+
describe('ensureXmrKeyPair()', () => {
|
|
494
|
+
beforeEach(() => {
|
|
495
|
+
config = new Config();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should generate and store RSA key pair', async () => {
|
|
499
|
+
expect(config.data.xmrPubKey).toBeUndefined();
|
|
500
|
+
expect(config.data.xmrPrivKey).toBeUndefined();
|
|
501
|
+
|
|
502
|
+
await config.ensureXmrKeyPair();
|
|
503
|
+
|
|
504
|
+
expect(config.data.xmrPubKey).toMatch(/^-----BEGIN PUBLIC KEY-----/);
|
|
505
|
+
expect(config.data.xmrPrivKey).toMatch(/^-----BEGIN PRIVATE KEY-----/);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should persist keys to localStorage', async () => {
|
|
509
|
+
await config.ensureXmrKeyPair();
|
|
510
|
+
|
|
511
|
+
const stored = JSON.parse(mockLocalStorage.getItem('xibo_config'));
|
|
512
|
+
expect(stored.xmrPubKey).toMatch(/^-----BEGIN PUBLIC KEY-----/);
|
|
513
|
+
expect(stored.xmrPrivKey).toMatch(/^-----BEGIN PRIVATE KEY-----/);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should be idempotent — second call preserves existing keys', async () => {
|
|
517
|
+
await config.ensureXmrKeyPair();
|
|
518
|
+
const firstPubKey = config.data.xmrPubKey;
|
|
519
|
+
const firstPrivKey = config.data.xmrPrivKey;
|
|
520
|
+
|
|
521
|
+
await config.ensureXmrKeyPair();
|
|
522
|
+
|
|
523
|
+
expect(config.data.xmrPubKey).toBe(firstPubKey);
|
|
524
|
+
expect(config.data.xmrPrivKey).toBe(firstPrivKey);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should regenerate keys if xmrPubKey is invalid', async () => {
|
|
528
|
+
config.data.xmrPubKey = 'invalid-key';
|
|
529
|
+
config.data.xmrPrivKey = 'invalid-key';
|
|
530
|
+
|
|
531
|
+
await config.ensureXmrKeyPair();
|
|
532
|
+
|
|
533
|
+
expect(config.data.xmrPubKey).toMatch(/^-----BEGIN PUBLIC KEY-----/);
|
|
534
|
+
expect(config.data.xmrPrivKey).toMatch(/^-----BEGIN PRIVATE KEY-----/);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should regenerate keys if xmrPubKey is empty string', async () => {
|
|
538
|
+
config.data.xmrPubKey = '';
|
|
539
|
+
|
|
540
|
+
await config.ensureXmrKeyPair();
|
|
541
|
+
|
|
542
|
+
expect(config.data.xmrPubKey).toMatch(/^-----BEGIN PUBLIC KEY-----/);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should survive config reload from localStorage', async () => {
|
|
546
|
+
await config.ensureXmrKeyPair();
|
|
547
|
+
const savedPubKey = config.data.xmrPubKey;
|
|
548
|
+
|
|
549
|
+
// Create new config (loads from localStorage)
|
|
550
|
+
const config2 = new Config();
|
|
551
|
+
|
|
552
|
+
expect(config2.data.xmrPubKey).toBe(savedPubKey);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe('XMR Key Getters', () => {
|
|
557
|
+
beforeEach(() => {
|
|
558
|
+
config = new Config();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('should return empty string for xmrPubKey when not set', () => {
|
|
562
|
+
expect(config.xmrPubKey).toBe('');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should return empty string for xmrPrivKey when not set', () => {
|
|
566
|
+
expect(config.xmrPrivKey).toBe('');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('should return xmrPubKey after ensureXmrKeyPair', async () => {
|
|
570
|
+
await config.ensureXmrKeyPair();
|
|
571
|
+
|
|
572
|
+
expect(config.xmrPubKey).toMatch(/^-----BEGIN PUBLIC KEY-----/);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('should return xmrPrivKey after ensureXmrKeyPair', async () => {
|
|
576
|
+
await config.ensureXmrKeyPair();
|
|
577
|
+
|
|
578
|
+
expect(config.xmrPrivKey).toMatch(/^-----BEGIN PRIVATE KEY-----/);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
473
581
|
});
|