@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.
- package/lib/commonjs/LicenseManager.js +887 -0
- package/lib/commonjs/api.js +198 -0
- package/lib/commonjs/config.js +26 -0
- package/lib/commonjs/device-info.js +93 -0
- package/lib/commonjs/fingerprint.js +30 -0
- package/lib/commonjs/hooks.js +216 -0
- package/lib/commonjs/index.js +103 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +1 -0
- package/lib/module/LicenseManager.js +884 -0
- package/lib/module/api.js +187 -0
- package/lib/module/config.js +22 -0
- package/lib/module/device-info.js +87 -0
- package/lib/module/fingerprint.js +10 -0
- package/lib/module/hooks.js +208 -0
- package/lib/module/index.js +43 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +1 -0
- package/lib/typescript/commonjs/LicenseManager.d.ts +132 -0
- package/lib/typescript/commonjs/LicenseManager.d.ts.map +1 -0
- package/lib/typescript/commonjs/api.d.ts +38 -0
- package/lib/typescript/commonjs/api.d.ts.map +1 -0
- package/lib/typescript/commonjs/config.d.ts +13 -0
- package/lib/typescript/commonjs/config.d.ts.map +1 -0
- package/lib/typescript/commonjs/device-info.d.ts +23 -0
- package/lib/typescript/commonjs/device-info.d.ts.map +1 -0
- package/lib/typescript/commonjs/fingerprint.d.ts +8 -0
- package/lib/typescript/commonjs/fingerprint.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks.d.ts +68 -0
- package/lib/typescript/commonjs/hooks.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +13 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/types.d.ts +200 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/module/LicenseManager.d.ts +132 -0
- package/lib/typescript/module/LicenseManager.d.ts.map +1 -0
- package/lib/typescript/module/api.d.ts +38 -0
- package/lib/typescript/module/api.d.ts.map +1 -0
- package/lib/typescript/module/config.d.ts +13 -0
- package/lib/typescript/module/config.d.ts.map +1 -0
- package/lib/typescript/module/device-info.d.ts +23 -0
- package/lib/typescript/module/device-info.d.ts.map +1 -0
- package/lib/typescript/module/fingerprint.d.ts +8 -0
- package/lib/typescript/module/fingerprint.d.ts.map +1 -0
- package/lib/typescript/module/hooks.d.ts +68 -0
- package/lib/typescript/module/hooks.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +13 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/types.d.ts +200 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- 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
|
+
}
|