@buoy-gg/license 1.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.
Files changed (53) hide show
  1. package/lib/commonjs/LicenseManager.js +887 -0
  2. package/lib/commonjs/api.js +198 -0
  3. package/lib/commonjs/config.js +26 -0
  4. package/lib/commonjs/device-info.js +93 -0
  5. package/lib/commonjs/fingerprint.js +30 -0
  6. package/lib/commonjs/hooks.js +216 -0
  7. package/lib/commonjs/index.js +103 -0
  8. package/lib/commonjs/package.json +1 -0
  9. package/lib/commonjs/types.js +1 -0
  10. package/lib/module/LicenseManager.js +884 -0
  11. package/lib/module/api.js +187 -0
  12. package/lib/module/config.js +22 -0
  13. package/lib/module/device-info.js +87 -0
  14. package/lib/module/fingerprint.js +10 -0
  15. package/lib/module/hooks.js +208 -0
  16. package/lib/module/index.js +43 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/module/types.js +1 -0
  19. package/lib/typescript/commonjs/LicenseManager.d.ts +132 -0
  20. package/lib/typescript/commonjs/LicenseManager.d.ts.map +1 -0
  21. package/lib/typescript/commonjs/api.d.ts +38 -0
  22. package/lib/typescript/commonjs/api.d.ts.map +1 -0
  23. package/lib/typescript/commonjs/config.d.ts +13 -0
  24. package/lib/typescript/commonjs/config.d.ts.map +1 -0
  25. package/lib/typescript/commonjs/device-info.d.ts +23 -0
  26. package/lib/typescript/commonjs/device-info.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/fingerprint.d.ts +8 -0
  28. package/lib/typescript/commonjs/fingerprint.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/hooks.d.ts +68 -0
  30. package/lib/typescript/commonjs/hooks.d.ts.map +1 -0
  31. package/lib/typescript/commonjs/index.d.ts +13 -0
  32. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  33. package/lib/typescript/commonjs/package.json +1 -0
  34. package/lib/typescript/commonjs/types.d.ts +200 -0
  35. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  36. package/lib/typescript/module/LicenseManager.d.ts +132 -0
  37. package/lib/typescript/module/LicenseManager.d.ts.map +1 -0
  38. package/lib/typescript/module/api.d.ts +38 -0
  39. package/lib/typescript/module/api.d.ts.map +1 -0
  40. package/lib/typescript/module/config.d.ts +13 -0
  41. package/lib/typescript/module/config.d.ts.map +1 -0
  42. package/lib/typescript/module/device-info.d.ts +23 -0
  43. package/lib/typescript/module/device-info.d.ts.map +1 -0
  44. package/lib/typescript/module/fingerprint.d.ts +8 -0
  45. package/lib/typescript/module/fingerprint.d.ts.map +1 -0
  46. package/lib/typescript/module/hooks.d.ts +68 -0
  47. package/lib/typescript/module/hooks.d.ts.map +1 -0
  48. package/lib/typescript/module/index.d.ts +13 -0
  49. package/lib/typescript/module/index.d.ts.map +1 -0
  50. package/lib/typescript/module/package.json +1 -0
  51. package/lib/typescript/module/types.d.ts +200 -0
  52. package/lib/typescript/module/types.d.ts.map +1 -0
  53. package/package.json +79 -0
@@ -0,0 +1,887 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.LicenseManager = void 0;
7
+ var _config = require("./config.js");
8
+ var _api = require("./api.js");
9
+ var _fingerprint = require("./fingerprint.js");
10
+ /**
11
+ * LicenseManager - Singleton for managing React Buoy Pro licenses
12
+ *
13
+ * Handles license validation, machine activation, caching, and state management.
14
+ *
15
+ * New flow:
16
+ * 1. validateLicenseKey() - Check if license is valid, returns existing devices
17
+ * 2. registerDevice(deviceName) - Register new device with user-provided name
18
+ * 3. claimDevice(machineId) - Re-claim an existing device after storage clear
19
+ */
20
+
21
+ // Note: We import types for the interface but use dynamic import for actual storage
22
+ // to avoid TypeScript rootDir issues with workspace dependencies during build
23
+
24
+ // Lazy load the storage module to avoid build issues
25
+ let _persistentStorage = null;
26
+ async function getPersistentStorage() {
27
+ if (_persistentStorage) return _persistentStorage;
28
+ try {
29
+ const mod = require("@buoy-gg/shared-ui/utils");
30
+ _persistentStorage = mod.persistentStorage;
31
+ return _persistentStorage;
32
+ } catch {
33
+ // Fallback to simple memory storage if shared-ui is not available
34
+ const memoryStore = new Map();
35
+ _persistentStorage = {
36
+ async getItem(key) {
37
+ return memoryStore.get(key) ?? null;
38
+ },
39
+ async setItem(key, value) {
40
+ memoryStore.set(key, value);
41
+ },
42
+ async removeItem(key) {
43
+ memoryStore.delete(key);
44
+ }
45
+ };
46
+ return _persistentStorage;
47
+ }
48
+ }
49
+ /**
50
+ * Result of license validation - indicates what action is needed
51
+ */
52
+
53
+ /**
54
+ * Result of device registration
55
+ */
56
+
57
+ class LicenseManagerImpl {
58
+ state = {
59
+ isInitialized: false,
60
+ isValidating: false,
61
+ isPro: false,
62
+ licenseKey: null,
63
+ error: null,
64
+ machineId: null,
65
+ maxMachines: null,
66
+ machineCount: 0,
67
+ expiresAt: null
68
+ };
69
+ listeners = new Set();
70
+ revalidationTimer = null;
71
+ cachedLicense = null;
72
+
73
+ // Temporary storage for license ID during registration flow
74
+ pendingLicenseId = null;
75
+
76
+ // Lock to prevent concurrent validation calls
77
+ validationInProgress = false;
78
+
79
+ // Lock to prevent concurrent registration calls
80
+ registrationInProgress = false;
81
+
82
+ // Promise for initialization to prevent double-init race condition
83
+ initPromise = null;
84
+
85
+ /**
86
+ * Initialize the license manager and load cached license
87
+ */
88
+ async initialize() {
89
+ // Return existing promise if already initializing
90
+ if (this.initPromise) return this.initPromise;
91
+ if (this.state.isInitialized) return;
92
+
93
+ // Store the promise so concurrent calls wait for the same initialization
94
+ this.initPromise = this._doInitialize();
95
+ return this.initPromise;
96
+ }
97
+
98
+ /**
99
+ * Internal initialization logic
100
+ */
101
+ async _doInitialize() {
102
+ if (this.state.isInitialized) return;
103
+ try {
104
+ // Load cached license from storage
105
+ const cached = await this.loadCachedLicense();
106
+ if (cached) {
107
+ this.cachedLicense = cached;
108
+ this.state.licenseKey = cached.licenseKey;
109
+ this.state.isPro = cached.isPro && this.isCacheValid(cached);
110
+ this.state.machineId = cached.machineId;
111
+ this.state.maxMachines = cached.maxMachines;
112
+ this.state.machineCount = cached.machineCount;
113
+ this.state.expiresAt = cached.expiresAt ? new Date(cached.expiresAt) : null;
114
+
115
+ // If we have a cached license with fingerprint, validate in background
116
+ if (cached.licenseKey && cached.fingerprint) {
117
+ this.validateInBackground(cached.licenseKey, cached.fingerprint);
118
+ }
119
+ }
120
+ this.state.isInitialized = true;
121
+ this.emit({
122
+ type: "initialized",
123
+ isPro: this.state.isPro
124
+ });
125
+
126
+ // Start periodic revalidation
127
+ this.startRevalidationTimer();
128
+ } catch (error) {
129
+ console.warn("[React Buoy] License initialization failed:", error);
130
+ this.state.isInitialized = true;
131
+ this.emit({
132
+ type: "initialized",
133
+ isPro: false
134
+ });
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Validate a license key and check what action is needed
140
+ * Returns info about whether device registration is needed
141
+ */
142
+ async validateLicenseKey(licenseKey) {
143
+ // Prevent concurrent validation calls
144
+ if (this.validationInProgress) {
145
+ return {
146
+ valid: false,
147
+ needsDeviceRegistration: false,
148
+ existingDevices: [],
149
+ error: "Validation already in progress"
150
+ };
151
+ }
152
+ if (!licenseKey || typeof licenseKey !== "string") {
153
+ return {
154
+ valid: false,
155
+ needsDeviceRegistration: false,
156
+ existingDevices: [],
157
+ error: "Invalid license key"
158
+ };
159
+ }
160
+
161
+ // Normalize license key
162
+ const normalizedKey = licenseKey.trim().toUpperCase();
163
+ this.state.licenseKey = normalizedKey;
164
+ this.state.isValidating = true;
165
+ this.state.error = null;
166
+ this.validationInProgress = true;
167
+ this.emit({
168
+ type: "validating",
169
+ isPro: this.state.isPro
170
+ });
171
+ try {
172
+ // Validate the license without fingerprint first
173
+ const response = await (0, _api.validateLicense)(normalizedKey);
174
+ const result = (0, _api.parseValidationResult)(response);
175
+
176
+ // Check for fatal errors
177
+ if ((0, _api.isFatalLicenseError)(result.code)) {
178
+ this.state.isValidating = false;
179
+ this.state.error = result.detail;
180
+ await this.clearCachedLicense();
181
+ this.emit({
182
+ type: "error",
183
+ isPro: false,
184
+ error: result.detail
185
+ });
186
+ return {
187
+ valid: false,
188
+ needsDeviceRegistration: false,
189
+ existingDevices: [],
190
+ error: result.detail
191
+ };
192
+ }
193
+
194
+ // If response.data is null, the license is invalid
195
+ if (!response.data) {
196
+ this.state.isValidating = false;
197
+ this.state.error = result.detail || "License not found";
198
+ await this.clearCachedLicense();
199
+ this.emit({
200
+ type: "error",
201
+ isPro: false,
202
+ error: this.state.error
203
+ });
204
+ return {
205
+ valid: false,
206
+ needsDeviceRegistration: false,
207
+ existingDevices: [],
208
+ error: this.state.error
209
+ };
210
+ }
211
+
212
+ // Store license ID for registration flow
213
+ this.pendingLicenseId = response.data.id;
214
+ this.state.maxMachines = response.data.attributes.maxMachines;
215
+ this.state.machineCount = this.countMachines(response);
216
+
217
+ // For simulators, we don't require device registration
218
+ const isSimulatorDevice = (0, _fingerprint.isSimulator)();
219
+ if (isSimulatorDevice) {
220
+ // For simulators, consider Pro if license is valid OR if only issue is no machines registered
221
+ const isPro = result.valid || result.code === "NO_MACHINES";
222
+ this.state.isPro = isPro;
223
+ this.state.isValidating = false;
224
+ if (isPro) {
225
+ await this.saveLicenseToCache(response, normalizedKey, null, null);
226
+ }
227
+ this.emit({
228
+ type: "validated",
229
+ isPro
230
+ });
231
+ return {
232
+ valid: isPro,
233
+ needsDeviceRegistration: false,
234
+ existingDevices: [],
235
+ maxDevices: this.state.maxMachines ?? undefined,
236
+ currentDeviceCount: this.state.machineCount,
237
+ error: isPro ? undefined : result.detail
238
+ };
239
+ }
240
+
241
+ // Check if we have cached fingerprint for this license
242
+ if (this.cachedLicense?.fingerprint && this.cachedLicense.licenseKey === normalizedKey) {
243
+ // Re-validate with our stored fingerprint
244
+ const fingerprintResponse = await (0, _api.validateLicense)(normalizedKey, this.cachedLicense.fingerprint);
245
+ const fingerprintResult = (0, _api.parseValidationResult)(fingerprintResponse);
246
+ if (fingerprintResult.valid) {
247
+ // Our device is still registered
248
+ this.state.isPro = true;
249
+ this.state.isValidating = false;
250
+ this.state.machineId = this.cachedLicense.machineId;
251
+ await this.saveLicenseToCache(fingerprintResponse, normalizedKey, this.cachedLicense.fingerprint, this.cachedLicense.deviceName ?? null);
252
+ this.emit({
253
+ type: "validated",
254
+ isPro: true
255
+ });
256
+ return {
257
+ valid: true,
258
+ needsDeviceRegistration: false,
259
+ existingDevices: [],
260
+ maxDevices: this.state.maxMachines ?? undefined,
261
+ currentDeviceCount: this.state.machineCount
262
+ };
263
+ }
264
+ }
265
+
266
+ // Get existing devices for this license
267
+ let existingDevices = [];
268
+ try {
269
+ const machines = await (0, _api.getLicenseMachines)(response.data.id, normalizedKey);
270
+ existingDevices = machines.map(machine => ({
271
+ id: machine.id,
272
+ fingerprint: machine.attributes.fingerprint,
273
+ name: machine.attributes.name || "Unknown Device",
274
+ platform: machine.attributes.platform || "unknown",
275
+ model: machine.attributes.metadata?.model || null,
276
+ osVersion: machine.attributes.metadata?.osVersion || null,
277
+ appVersion: machine.attributes.metadata?.appVersion || null,
278
+ registeredAt: new Date(machine.attributes.created),
279
+ lastSeenAt: new Date(machine.attributes.updated),
280
+ isCurrentDevice: false // We don't have a fingerprint yet
281
+ }));
282
+ } catch {
283
+ // Failed to get devices, continue anyway
284
+ }
285
+
286
+ // Check if device registration is needed
287
+ // Device registration is needed if:
288
+ // 1. API says no machines (NO_MACHINE/NO_MACHINES code), OR
289
+ // 2. Too many machines (need to remove one), OR
290
+ // 3. This device has no cached fingerprint (meaning it's not registered yet)
291
+ const noMachinesOnLicense = (0, _api.needsMachineActivation)(result.code);
292
+ const tooManyMachines = (0, _api.hasTooManyMachines)(result.code);
293
+ const thisDeviceNotRegistered = !this.cachedLicense?.fingerprint;
294
+ const needsRegistration = noMachinesOnLicense || tooManyMachines || thisDeviceNotRegistered;
295
+ this.state.isValidating = false;
296
+ if ((0, _api.hasTooManyMachines)(result.code)) {
297
+ return {
298
+ valid: true,
299
+ // License is valid, just at device limit
300
+ needsDeviceRegistration: true,
301
+ existingDevices,
302
+ error: "Device limit reached. Remove an existing device or claim one.",
303
+ maxDevices: this.state.maxMachines ?? undefined,
304
+ currentDeviceCount: this.state.machineCount
305
+ };
306
+ }
307
+ return {
308
+ valid: true,
309
+ needsDeviceRegistration: needsRegistration,
310
+ existingDevices,
311
+ maxDevices: this.state.maxMachines ?? undefined,
312
+ currentDeviceCount: this.state.machineCount
313
+ };
314
+ } catch (error) {
315
+ const errorMessage = error instanceof Error ? error.message : "Validation failed";
316
+
317
+ // Check for network error and use cache
318
+ if (this.isNetworkError(error) && this.cachedLicense && this.isCacheValid(this.cachedLicense)) {
319
+ this.state.isPro = this.cachedLicense.isPro;
320
+ this.state.isValidating = false;
321
+ this.emit({
322
+ type: "validated",
323
+ isPro: this.state.isPro
324
+ });
325
+ return {
326
+ valid: this.state.isPro,
327
+ needsDeviceRegistration: false,
328
+ existingDevices: []
329
+ };
330
+ }
331
+ this.state.isValidating = false;
332
+ this.state.error = errorMessage;
333
+ this.emit({
334
+ type: "error",
335
+ isPro: false,
336
+ error: errorMessage
337
+ });
338
+ return {
339
+ valid: false,
340
+ needsDeviceRegistration: false,
341
+ existingDevices: [],
342
+ error: errorMessage
343
+ };
344
+ } finally {
345
+ this.validationInProgress = false;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Register a new device with a user-provided name
351
+ */
352
+ async registerDevice(deviceName) {
353
+ // Prevent concurrent registration calls
354
+ if (this.registrationInProgress) {
355
+ const error = "Registration already in progress";
356
+ this.state.error = error;
357
+ return {
358
+ success: false,
359
+ error
360
+ };
361
+ }
362
+
363
+ // Wait if validation is still in progress
364
+ if (this.validationInProgress) {
365
+ const error = "Please wait for license validation to complete";
366
+ this.state.error = error;
367
+ return {
368
+ success: false,
369
+ error
370
+ };
371
+ }
372
+ if (!this.state.licenseKey || !this.pendingLicenseId) {
373
+ const error = "No license key set. Call validateLicenseKey first.";
374
+ this.state.error = error;
375
+ return {
376
+ success: false,
377
+ error
378
+ };
379
+ }
380
+ if (!deviceName || deviceName.trim().length === 0) {
381
+ const error = "Device name is required";
382
+ this.state.error = error;
383
+ return {
384
+ success: false,
385
+ error
386
+ };
387
+ }
388
+ this.registrationInProgress = true;
389
+ this.state.isValidating = true;
390
+ this.emit({
391
+ type: "validating",
392
+ isPro: this.state.isPro
393
+ });
394
+ try {
395
+ // Generate a new fingerprint for this device
396
+ const fingerprint = (0, _fingerprint.generateUUID)();
397
+ const metadata = (0, _fingerprint.getDeviceMetadata)();
398
+
399
+ // Register the device with Keygen
400
+ const response = await (0, _api.activateMachine)(this.pendingLicenseId, this.state.licenseKey, fingerprint, deviceName.trim(), metadata);
401
+ this.state.machineId = response.data.id;
402
+
403
+ // Validate with the new fingerprint to get updated machine count
404
+ const validateResponse = await (0, _api.validateLicense)(this.state.licenseKey, fingerprint);
405
+
406
+ // If activateMachine succeeded, the device is registered - trust that over validation result
407
+ // (validation might fail due to API timing but activation success is authoritative)
408
+ this.state.isPro = true;
409
+ this.state.isValidating = false;
410
+ this.state.machineCount = this.countMachines(validateResponse);
411
+
412
+ // Save to cache with fingerprint and device name
413
+ await this.saveLicenseToCache(validateResponse, this.state.licenseKey, fingerprint, deviceName.trim());
414
+ this.emit({
415
+ type: "activated",
416
+ isPro: true
417
+ });
418
+ return {
419
+ success: true
420
+ };
421
+ } catch (error) {
422
+ const errorMessage = error instanceof Error ? error.message : "Registration failed";
423
+ let userError;
424
+ if (errorMessage === "MACHINE_LIMIT_EXCEEDED") {
425
+ userError = "Device limit reached. Remove an existing device first.";
426
+ } else if (errorMessage === "FINGERPRINT_TAKEN") {
427
+ // This shouldn't happen with UUID, but handle it anyway
428
+ userError = "Device ID conflict. Please try again.";
429
+ } else {
430
+ userError = errorMessage;
431
+ }
432
+ this.state.error = userError;
433
+ this.state.isValidating = false;
434
+ this.emit({
435
+ type: "error",
436
+ isPro: false,
437
+ error: userError
438
+ });
439
+ return {
440
+ success: false,
441
+ error: userError
442
+ };
443
+ } finally {
444
+ this.registrationInProgress = false;
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Claim an existing device (use its fingerprint)
450
+ * This is for when the user wants to re-use a device slot after clearing storage
451
+ */
452
+ async claimDevice(machineId, existingDevices) {
453
+ if (!this.state.licenseKey) {
454
+ this.state.error = "No license key set";
455
+ return false;
456
+ }
457
+
458
+ // First try passed devices, then fetch fresh if not found (handles stale data)
459
+ let device = existingDevices.find(d => d.id === machineId);
460
+ if (!device) {
461
+ // Fetch fresh devices in case the list is stale
462
+ const freshDevices = await this.getRegisteredDevices();
463
+ device = freshDevices.find(d => d.id === machineId);
464
+ }
465
+ if (!device) {
466
+ this.state.error = "Device not found";
467
+ return false;
468
+ }
469
+ this.state.isValidating = true;
470
+ this.emit({
471
+ type: "validating",
472
+ isPro: this.state.isPro
473
+ });
474
+ try {
475
+ // Validate with the claimed device's fingerprint
476
+ const response = await (0, _api.validateLicense)(this.state.licenseKey, device.fingerprint);
477
+ const result = (0, _api.parseValidationResult)(response);
478
+ if (!result.valid) {
479
+ this.state.isValidating = false;
480
+ this.state.error = "Failed to claim device";
481
+ this.emit({
482
+ type: "error",
483
+ isPro: false,
484
+ error: this.state.error
485
+ });
486
+ return false;
487
+ }
488
+ this.state.isPro = true;
489
+ this.state.machineId = machineId;
490
+ this.state.isValidating = false;
491
+ this.state.machineCount = this.countMachines(response);
492
+
493
+ // Save to cache with the claimed fingerprint
494
+ await this.saveLicenseToCache(response, this.state.licenseKey, device.fingerprint, device.name);
495
+ this.emit({
496
+ type: "validated",
497
+ isPro: true
498
+ });
499
+ return true;
500
+ } catch (error) {
501
+ const errorMessage = error instanceof Error ? error.message : "Failed to claim device";
502
+ this.state.isValidating = false;
503
+ this.state.error = errorMessage;
504
+ this.emit({
505
+ type: "error",
506
+ isPro: false,
507
+ error: errorMessage
508
+ });
509
+ return false;
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Legacy method - validates and auto-registers if needed (for backward compatibility)
515
+ * Note: This won't work well with the new flow - use validateLicenseKey + registerDevice instead
516
+ */
517
+ async setLicenseKey(licenseKey) {
518
+ const result = await this.validateLicenseKey(licenseKey);
519
+ if (!result.valid) {
520
+ return false;
521
+ }
522
+ if (!result.needsDeviceRegistration) {
523
+ return true;
524
+ }
525
+
526
+ // Auto-register with default name (legacy behavior)
527
+ const defaultName = `Device ${Date.now()}`;
528
+ const regResult = await this.registerDevice(defaultName);
529
+ return regResult.success;
530
+ }
531
+
532
+ /**
533
+ * Check if an error is a network error (not an API validation error)
534
+ */
535
+ isNetworkError(error) {
536
+ if (error instanceof TypeError) {
537
+ return true;
538
+ }
539
+ if (error instanceof Error) {
540
+ const message = error.message.toLowerCase();
541
+ return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("connection") || message.includes("offline") || message.includes("failed to fetch");
542
+ }
543
+ return false;
544
+ }
545
+
546
+ /**
547
+ * Clear the current license
548
+ */
549
+ async clearLicense() {
550
+ // If we have a machine ID and license key, try to deactivate it
551
+ if (this.state.machineId && this.state.licenseKey) {
552
+ try {
553
+ await (0, _api.deactivateMachine)(this.state.machineId, this.state.licenseKey);
554
+ } catch {
555
+ // Ignore deactivation errors
556
+ }
557
+ }
558
+ this.state.licenseKey = null;
559
+ this.state.isPro = false;
560
+ this.state.machineId = null;
561
+ this.state.maxMachines = null;
562
+ this.state.machineCount = 0;
563
+ this.state.expiresAt = null;
564
+ this.state.error = null;
565
+ this.cachedLicense = null;
566
+ this.pendingLicenseId = null;
567
+ await this.clearCachedLicense();
568
+ this.emit({
569
+ type: "deactivated",
570
+ isPro: false
571
+ });
572
+ }
573
+
574
+ /**
575
+ * Check if the user has Pro features
576
+ */
577
+ isPro() {
578
+ return this.state.isPro;
579
+ }
580
+
581
+ /**
582
+ * Check if a specific entitlement is available
583
+ */
584
+ hasEntitlement(code) {
585
+ if (code === "PRO") {
586
+ return this.state.isPro;
587
+ }
588
+ return this.state.isPro;
589
+ }
590
+
591
+ /**
592
+ * Get current license state
593
+ */
594
+ getState() {
595
+ return {
596
+ ...this.state
597
+ };
598
+ }
599
+
600
+ /**
601
+ * Get seat information
602
+ */
603
+ getSeatsInfo() {
604
+ return {
605
+ used: this.state.machineCount,
606
+ total: this.state.maxMachines
607
+ };
608
+ }
609
+
610
+ /**
611
+ * Subscribe to license events
612
+ */
613
+ subscribe(listener) {
614
+ this.listeners.add(listener);
615
+ return () => this.listeners.delete(listener);
616
+ }
617
+
618
+ /**
619
+ * Get all registered devices for the current license
620
+ */
621
+ async getRegisteredDevices() {
622
+ const licenseId = this.cachedLicense?.licenseId || this.pendingLicenseId;
623
+ if (!licenseId || !this.state.licenseKey) {
624
+ return [];
625
+ }
626
+ try {
627
+ const currentFingerprint = this.cachedLicense?.fingerprint;
628
+ const machines = await (0, _api.getLicenseMachines)(licenseId, this.state.licenseKey);
629
+ return machines.map(machine => ({
630
+ id: machine.id,
631
+ fingerprint: machine.attributes.fingerprint,
632
+ name: machine.attributes.name || "Unknown Device",
633
+ platform: machine.attributes.platform || "unknown",
634
+ model: machine.attributes.metadata?.model || null,
635
+ osVersion: machine.attributes.metadata?.osVersion || null,
636
+ appVersion: machine.attributes.metadata?.appVersion || null,
637
+ registeredAt: new Date(machine.attributes.created),
638
+ lastSeenAt: new Date(machine.attributes.updated),
639
+ isCurrentDevice: currentFingerprint ? machine.attributes.fingerprint === currentFingerprint : false
640
+ }));
641
+ } catch {
642
+ return [];
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Deactivate a specific device by machine ID
648
+ */
649
+ async deactivateDevice(machineId) {
650
+ if (!this.state.licenseKey) {
651
+ return false;
652
+ }
653
+ try {
654
+ await (0, _api.deactivateMachine)(machineId, this.state.licenseKey);
655
+
656
+ // Update machine count
657
+ this.state.machineCount = Math.max(0, this.state.machineCount - 1);
658
+
659
+ // If we deactivated the current device, clear our registration
660
+ if (machineId === this.state.machineId) {
661
+ this.state.machineId = null;
662
+ if (this.cachedLicense) {
663
+ this.cachedLicense.machineId = null;
664
+ this.cachedLicense.fingerprint = undefined;
665
+ this.cachedLicense.deviceName = undefined;
666
+ this.cachedLicense.machineCount = this.state.machineCount;
667
+ try {
668
+ const storage = await getPersistentStorage();
669
+ await storage.setItem(_config.LICENSE_STORAGE_KEY, JSON.stringify(this.cachedLicense));
670
+ } catch {
671
+ // Ignore storage errors
672
+ }
673
+ }
674
+ this.emit({
675
+ type: "deactivated",
676
+ isPro: this.state.isPro
677
+ });
678
+ } else {
679
+ // Update cache with new machine count
680
+ if (this.cachedLicense) {
681
+ this.cachedLicense.machineCount = this.state.machineCount;
682
+ try {
683
+ const storage = await getPersistentStorage();
684
+ await storage.setItem(_config.LICENSE_STORAGE_KEY, JSON.stringify(this.cachedLicense));
685
+ } catch {
686
+ // Ignore storage errors
687
+ }
688
+ }
689
+ this.emit({
690
+ type: "validated",
691
+ isPro: this.state.isPro
692
+ });
693
+ }
694
+ return true;
695
+ } catch (error) {
696
+ console.warn("[React Buoy] Failed to deactivate device:", error);
697
+ return false;
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Register the current device for the license (legacy - use registerDevice instead)
703
+ */
704
+ async registerCurrentDevice() {
705
+ if (!this.state.licenseKey || !this.pendingLicenseId) {
706
+ return false;
707
+ }
708
+ const defaultName = `Device ${Date.now()}`;
709
+ const result = await this.registerDevice(defaultName);
710
+ return result.success;
711
+ }
712
+
713
+ /**
714
+ * Check if the current device is registered
715
+ */
716
+ isCurrentDeviceRegistered() {
717
+ return this.state.machineId !== null;
718
+ }
719
+
720
+ // Private methods
721
+
722
+ emit(event) {
723
+ // Iterate over a copy to prevent issues if listeners modify the Set
724
+ const listeners = [...this.listeners];
725
+ listeners.forEach(listener => {
726
+ try {
727
+ listener(event);
728
+ } catch (error) {
729
+ console.warn("[React Buoy] License listener error:", error);
730
+ }
731
+ });
732
+ }
733
+ countMachines(response) {
734
+ const metaCount = response.data?.relationships?.machines?.meta?.count;
735
+ if (typeof metaCount === "number") {
736
+ return metaCount;
737
+ }
738
+ const relationshipCount = response.data?.relationships?.machines?.data?.length;
739
+ if (relationshipCount !== undefined && relationshipCount > 0) {
740
+ return relationshipCount;
741
+ }
742
+ if (response.included) {
743
+ const machines = response.included.filter(item => item.type === "machines");
744
+ if (machines.length > 0) {
745
+ return machines.length;
746
+ }
747
+ }
748
+ return 0;
749
+ }
750
+ isCacheValid(cached) {
751
+ const now = Date.now();
752
+ const cacheAge = now - cached.cachedAt;
753
+ if (cacheAge > _config.LICENSE_CACHE_DURATION) {
754
+ return false;
755
+ }
756
+ if (cached.expiresAt) {
757
+ const expiry = new Date(cached.expiresAt).getTime();
758
+ if (now > expiry) {
759
+ return false;
760
+ }
761
+ }
762
+ return true;
763
+ }
764
+ async loadCachedLicense() {
765
+ try {
766
+ const storage = await getPersistentStorage();
767
+ const data = await storage.getItem(_config.LICENSE_STORAGE_KEY);
768
+ if (!data) return null;
769
+ const cached = JSON.parse(data);
770
+ return cached;
771
+ } catch {
772
+ return null;
773
+ }
774
+ }
775
+ async saveLicenseToCache(response, licenseKey, fingerprint, deviceName) {
776
+ if (!response.data) {
777
+ return;
778
+ }
779
+ const cached = {
780
+ licenseKey,
781
+ licenseId: response.data.id,
782
+ valid: response.meta.valid,
783
+ isPro: response.meta.valid || response.meta.code === "NO_MACHINES",
784
+ entitlements: ["PRO"],
785
+ machineId: this.state.machineId,
786
+ maxMachines: response.data.attributes.maxMachines,
787
+ machineCount: this.countMachines(response),
788
+ expiresAt: response.data.attributes.expiry,
789
+ cachedAt: Date.now(),
790
+ lastValidatedAt: Date.now(),
791
+ fingerprint: fingerprint ?? undefined,
792
+ deviceName: deviceName ?? undefined
793
+ };
794
+ this.cachedLicense = cached;
795
+ try {
796
+ const storage = await getPersistentStorage();
797
+ await storage.setItem(_config.LICENSE_STORAGE_KEY, JSON.stringify(cached));
798
+ } catch (error) {
799
+ console.warn("[React Buoy] Failed to cache license:", error);
800
+ }
801
+ }
802
+ async clearCachedLicense() {
803
+ this.cachedLicense = null;
804
+ try {
805
+ const storage = await getPersistentStorage();
806
+ await storage.removeItem(_config.LICENSE_STORAGE_KEY);
807
+ } catch {
808
+ // Ignore
809
+ }
810
+ }
811
+ validateInBackground(licenseKey, fingerprint) {
812
+ (0, _api.validateLicense)(licenseKey, fingerprint).then(async response => {
813
+ const result = (0, _api.parseValidationResult)(response);
814
+ if (result.valid) {
815
+ this.state.isPro = true;
816
+ this.state.machineCount = this.countMachines(response);
817
+ this.emit({
818
+ type: "validated",
819
+ isPro: true
820
+ });
821
+ } else {
822
+ // Device was removed by admin or license revoked
823
+ console.warn("[React Buoy] Background validation: device no longer valid");
824
+ this.state.isPro = false;
825
+ this.state.machineId = null;
826
+ await this.clearCachedLicense();
827
+ this.emit({
828
+ type: "deactivated",
829
+ isPro: false
830
+ });
831
+ }
832
+ }).catch(error => {
833
+ console.warn("[React Buoy] Background validation failed:", error);
834
+ });
835
+ }
836
+ startRevalidationTimer() {
837
+ if (this.revalidationTimer) {
838
+ clearInterval(this.revalidationTimer);
839
+ }
840
+ this.revalidationTimer = setInterval(() => {
841
+ if (this.state.licenseKey && this.cachedLicense?.fingerprint && !this.state.isValidating) {
842
+ this.validateInBackground(this.state.licenseKey, this.cachedLicense.fingerprint);
843
+ }
844
+ }, _config.LICENSE_REVALIDATION_INTERVAL);
845
+
846
+ // Store for hot reload cleanup
847
+ if (__DEV__) {
848
+ global.__LICENSE_MANAGER_TIMER__ = this.revalidationTimer;
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Stop the revalidation timer
854
+ */
855
+ stopRevalidationTimer() {
856
+ if (this.revalidationTimer) {
857
+ clearInterval(this.revalidationTimer);
858
+ this.revalidationTimer = null;
859
+ }
860
+ }
861
+
862
+ /**
863
+ * Clear cache and revalidate
864
+ */
865
+ async clearCacheAndRevalidate() {
866
+ const currentKey = this.state.licenseKey;
867
+ await this.clearCachedLicense();
868
+ this.state.machineCount = 0;
869
+ if (currentKey) {
870
+ const result = await this.validateLicenseKey(currentKey);
871
+ return result.valid && !result.needsDeviceRegistration;
872
+ }
873
+ return false;
874
+ }
875
+ }
876
+
877
+ // Singleton instance
878
+ const LicenseManager = exports.LicenseManager = new LicenseManagerImpl();
879
+
880
+ // Hot reload cleanup - prevent timer leaks during development
881
+
882
+ if (__DEV__) {
883
+ // Clear any existing timer from previous hot reload
884
+ if (global.__LICENSE_MANAGER_TIMER__) {
885
+ clearInterval(global.__LICENSE_MANAGER_TIMER__);
886
+ }
887
+ }