@xiboplayer/utils 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.
package/src/config.js ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Configuration management with priority: env vars → localStorage → defaults
3
+ *
4
+ * In Node.js (tests, CLI): environment variables are the only source.
5
+ * In browser (PWA player): localStorage is primary, env vars override if set.
6
+ */
7
+
8
+ const STORAGE_KEY = 'xibo_config';
9
+ const HW_DB_NAME = 'xibo-hw-backup';
10
+ const HW_DB_VERSION = 1;
11
+
12
+ /**
13
+ * Check for environment variable config (highest priority).
14
+ * Env vars: CMS_ADDRESS, CMS_KEY, DISPLAY_NAME, HARDWARE_KEY, XMR_CHANNEL
15
+ * Returns config object if any env vars are set, null otherwise.
16
+ */
17
+ function loadFromEnv() {
18
+ // Check if process.env is available (Node.js or bundler injection)
19
+ const env = typeof process !== 'undefined' && process.env ? process.env : {};
20
+
21
+ const envConfig = {
22
+ cmsAddress: env.CMS_ADDRESS || env.CMS_URL || '',
23
+ cmsKey: env.CMS_KEY || '',
24
+ displayName: env.DISPLAY_NAME || '',
25
+ hardwareKey: env.HARDWARE_KEY || '',
26
+ xmrChannel: env.XMR_CHANNEL || '',
27
+ };
28
+
29
+ // Return env config if any value is set
30
+ const hasEnvValues = Object.values(envConfig).some(v => v !== '');
31
+ return hasEnvValues ? envConfig : null;
32
+ }
33
+
34
+ export class Config {
35
+ constructor() {
36
+ this.data = this.load();
37
+ // Async: try to restore hardware key from IndexedDB if localStorage lost it
38
+ // (only when not running from env vars)
39
+ if (!this._fromEnv) {
40
+ this._restoreHardwareKeyFromBackup();
41
+ }
42
+ }
43
+
44
+ load() {
45
+ // Priority 1: Environment variables (Node.js, tests, CI)
46
+ const envConfig = loadFromEnv();
47
+ if (envConfig) {
48
+ this._fromEnv = true;
49
+ return envConfig;
50
+ }
51
+
52
+ // Priority 2: localStorage (browser)
53
+ if (typeof localStorage === 'undefined') {
54
+ return { cmsAddress: '', cmsKey: '', displayName: '', hardwareKey: '', xmrChannel: '' };
55
+ }
56
+
57
+ // Try to load from localStorage
58
+ const json = localStorage.getItem(STORAGE_KEY);
59
+
60
+ if (json) {
61
+ try {
62
+ const config = JSON.parse(json);
63
+
64
+ // CRITICAL: Hardware key must persist
65
+ if (!config.hardwareKey || config.hardwareKey.length < 10) {
66
+ console.error('[Config] CRITICAL: Invalid/missing hardwareKey in localStorage!');
67
+ config.hardwareKey = this.generateStableHardwareKey();
68
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
69
+ this._backupHardwareKey(config.hardwareKey);
70
+ } else {
71
+ console.log('[Config] ✓ Loaded existing hardwareKey:', config.hardwareKey);
72
+ }
73
+
74
+ return config;
75
+ } catch (e) {
76
+ console.error('[Config] Failed to parse config from localStorage:', e);
77
+ // Fall through to create new config
78
+ }
79
+ }
80
+
81
+ // No config in localStorage - first time setup
82
+ console.log('[Config] No config in localStorage - first time setup');
83
+
84
+ const newConfig = {
85
+ cmsAddress: '',
86
+ cmsKey: '',
87
+ displayName: '',
88
+ hardwareKey: this.generateStableHardwareKey(),
89
+ xmrChannel: this.generateXmrChannel()
90
+ };
91
+
92
+ // Save immediately
93
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newConfig));
94
+ this._backupHardwareKey(newConfig.hardwareKey);
95
+ console.log('[Config] ✓ Saved new config to localStorage');
96
+ console.log('[Config] Hardware key will persist across reloads:', newConfig.hardwareKey);
97
+
98
+ return newConfig;
99
+ }
100
+
101
+ /**
102
+ * Backup hardware key to IndexedDB (more persistent than localStorage).
103
+ * IndexedDB survives "Clear site data" in some browsers where localStorage doesn't.
104
+ */
105
+ _backupHardwareKey(key) {
106
+ try {
107
+ const req = indexedDB.open(HW_DB_NAME, HW_DB_VERSION);
108
+ req.onupgradeneeded = () => {
109
+ const db = req.result;
110
+ if (!db.objectStoreNames.contains('keys')) {
111
+ db.createObjectStore('keys');
112
+ }
113
+ };
114
+ req.onsuccess = () => {
115
+ const db = req.result;
116
+ const tx = db.transaction('keys', 'readwrite');
117
+ tx.objectStore('keys').put(key, 'hardwareKey');
118
+ tx.oncomplete = () => {
119
+ console.log('[Config] Hardware key backed up to IndexedDB');
120
+ db.close();
121
+ };
122
+ };
123
+ } catch (e) {
124
+ // IndexedDB not available — localStorage-only mode
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Restore hardware key from IndexedDB if localStorage was cleared.
130
+ * Runs async after construction — if a backed-up key is found and
131
+ * differs from the current one, it restores the original key.
132
+ */
133
+ async _restoreHardwareKeyFromBackup() {
134
+ if (typeof indexedDB === 'undefined') return;
135
+ try {
136
+ const db = await new Promise((resolve, reject) => {
137
+ const req = indexedDB.open(HW_DB_NAME, HW_DB_VERSION);
138
+ req.onupgradeneeded = () => {
139
+ const db = req.result;
140
+ if (!db.objectStoreNames.contains('keys')) {
141
+ db.createObjectStore('keys');
142
+ }
143
+ };
144
+ req.onsuccess = () => resolve(req.result);
145
+ req.onerror = () => reject(req.error);
146
+ });
147
+
148
+ const tx = db.transaction('keys', 'readonly');
149
+ const store = tx.objectStore('keys');
150
+ const backedUpKey = await new Promise((resolve) => {
151
+ const req = store.get('hardwareKey');
152
+ req.onsuccess = () => resolve(req.result);
153
+ req.onerror = () => resolve(null);
154
+ });
155
+ db.close();
156
+
157
+ if (backedUpKey && backedUpKey !== this.data.hardwareKey) {
158
+ console.log('[Config] Restoring hardware key from IndexedDB backup:', backedUpKey);
159
+ console.log('[Config] (was:', this.data.hardwareKey, ')');
160
+ this.data.hardwareKey = backedUpKey;
161
+ this.save();
162
+ } else if (!backedUpKey && this.data.hardwareKey) {
163
+ // No backup yet — save current key as backup
164
+ this._backupHardwareKey(this.data.hardwareKey);
165
+ }
166
+ } catch (e) {
167
+ // IndexedDB not available — that's fine
168
+ }
169
+ }
170
+
171
+ save() {
172
+ if (typeof localStorage !== 'undefined') {
173
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data));
174
+ }
175
+ }
176
+
177
+ isConfigured() {
178
+ return !!(this.data.cmsAddress && this.data.cmsKey && this.data.displayName);
179
+ }
180
+
181
+ generateStableHardwareKey() {
182
+ // Generate a stable UUID-based hardware key
183
+ // CRITICAL: This is generated ONCE and saved to localStorage
184
+ // It NEVER changes unless localStorage is cleared manually
185
+
186
+ // Use crypto.randomUUID if available (best randomness)
187
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
188
+ const uuid = crypto.randomUUID().replace(/-/g, ''); // Remove dashes
189
+ const hardwareKey = 'pwa-' + uuid.substring(0, 28);
190
+ console.log('[Config] Generated new UUID-based hardware key:', hardwareKey);
191
+ return hardwareKey;
192
+ }
193
+
194
+ // Fallback: Generate random hex string
195
+ const randomHex = Array.from({ length: 28 }, () =>
196
+ Math.floor(Math.random() * 16).toString(16)
197
+ ).join('');
198
+
199
+ const hardwareKey = 'pwa-' + randomHex;
200
+ console.log('[Config] Generated new random hardware key:', hardwareKey);
201
+ return hardwareKey;
202
+ }
203
+
204
+ getCanvasFingerprint() {
205
+ // Generate stable canvas fingerprint (same for same GPU/driver)
206
+ try {
207
+ const canvas = document.createElement('canvas');
208
+ const ctx = canvas.getContext('2d');
209
+ if (!ctx) return 'no-canvas';
210
+
211
+ // Draw test pattern (same rendering = same device)
212
+ ctx.textBaseline = 'top';
213
+ ctx.font = '14px Arial';
214
+ ctx.fillStyle = '#f60';
215
+ ctx.fillRect(125, 1, 62, 20);
216
+ ctx.fillStyle = '#069';
217
+ ctx.fillText('Xibo Player', 2, 15);
218
+
219
+ return canvas.toDataURL();
220
+ } catch (e) {
221
+ return 'canvas-error';
222
+ }
223
+ }
224
+
225
+ generateHardwareKey() {
226
+ // For backwards compatibility
227
+ return this.generateStableHardwareKey();
228
+ }
229
+
230
+ generateXmrChannel() {
231
+ // Generate UUID for XMR channel
232
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
233
+ const r = Math.random() * 16 | 0;
234
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
235
+ return v.toString(16);
236
+ });
237
+ }
238
+
239
+ hash(str) {
240
+ // FNV-1a hash algorithm (better distribution than simple hash)
241
+ // Produces high-entropy 32-character hex string
242
+ let hash = 2166136261; // FNV offset basis
243
+
244
+ for (let i = 0; i < str.length; i++) {
245
+ hash ^= str.charCodeAt(i);
246
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
247
+ }
248
+
249
+ // Convert to unsigned 32-bit integer
250
+ hash = hash >>> 0;
251
+
252
+ // Extend to 32 characters by hashing multiple times with different seeds
253
+ let result = '';
254
+ for (let round = 0; round < 4; round++) {
255
+ let roundHash = hash + round * 1234567;
256
+ for (let i = 0; i < str.length; i++) {
257
+ roundHash ^= str.charCodeAt(i) + round;
258
+ roundHash += (roundHash << 1) + (roundHash << 4) + (roundHash << 7) + (roundHash << 8) + (roundHash << 24);
259
+ }
260
+ roundHash = roundHash >>> 0;
261
+ result += roundHash.toString(16).padStart(8, '0');
262
+ }
263
+
264
+ return result.substring(0, 32);
265
+ }
266
+
267
+ get cmsAddress() { return this.data.cmsAddress; }
268
+ set cmsAddress(val) { this.data.cmsAddress = val; this.save(); }
269
+
270
+ get cmsKey() { return this.data.cmsKey; }
271
+ set cmsKey(val) { this.data.cmsKey = val; this.save(); }
272
+
273
+ get displayName() { return this.data.displayName; }
274
+ set displayName(val) { this.data.displayName = val; this.save(); }
275
+
276
+ get hardwareKey() {
277
+ // CRITICAL: Ensure hardware key never becomes undefined
278
+ if (!this.data.hardwareKey) {
279
+ console.error('[Config] CRITICAL: hardwareKey missing! Generating emergency key.');
280
+ this.data.hardwareKey = this.generateStableHardwareKey();
281
+ this.save();
282
+ }
283
+ return this.data.hardwareKey;
284
+ }
285
+ get xmrChannel() { return this.data.xmrChannel; }
286
+ }
287
+
288
+ export const config = new Config();