@buoy-gg/license 7.0.21 → 7.0.23
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/LICENSE +46 -0
- package/lib/commonjs/DeviceRegistrationModal.js +1 -189
- package/lib/commonjs/LicenseManager.js +14 -610
- package/lib/commonjs/api.js +1 -172
- package/lib/commonjs/cacheSignature.js +15 -0
- package/lib/commonjs/config.js +16 -28
- package/lib/commonjs/device-info.js +1 -93
- package/lib/commonjs/device-info.web.js +1 -227
- package/lib/commonjs/fingerprint.js +1 -30
- package/lib/commonjs/fingerprint.web.js +1 -30
- package/lib/commonjs/hooks.js +13 -120
- package/lib/commonjs/index.js +17 -106
- package/lib/commonjs/index.web.js +17 -98
- package/lib/commonjs/tier.js +16 -0
- package/lib/commonjs/types.js +1 -1
- package/lib/commonjs/weekendPro.js +15 -41
- package/lib/module/DeviceRegistrationModal.js +1 -185
- package/lib/module/LicenseManager.js +14 -607
- package/lib/module/api.js +1 -165
- package/lib/module/cacheSignature.js +15 -0
- package/lib/module/config.js +16 -24
- package/lib/module/device-info.js +1 -87
- package/lib/module/device-info.web.js +1 -220
- package/lib/module/fingerprint.js +1 -10
- package/lib/module/fingerprint.web.js +1 -10
- package/lib/module/hooks.js +13 -114
- package/lib/module/index.js +15 -54
- package/lib/module/index.web.js +15 -52
- package/lib/module/tier.js +16 -0
- package/lib/module/types.js +1 -1
- package/lib/module/weekendPro.js +15 -37
- package/lib/typescript/commonjs/LicenseManager.d.ts +31 -6
- package/lib/typescript/commonjs/LicenseManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/cacheSignature.d.ts +42 -0
- package/lib/typescript/commonjs/cacheSignature.d.ts.map +1 -0
- package/lib/typescript/commonjs/config.d.ts +62 -1
- package/lib/typescript/commonjs/config.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks.d.ts +27 -5
- package/lib/typescript/commonjs/hooks.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +20 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +20 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tier.d.ts +55 -0
- package/lib/typescript/commonjs/tier.d.ts.map +1 -0
- package/lib/typescript/commonjs/types.d.ts +44 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/weekendPro.d.ts +30 -7
- package/lib/typescript/commonjs/weekendPro.d.ts.map +1 -1
- package/lib/typescript/module/LicenseManager.d.ts +31 -6
- package/lib/typescript/module/LicenseManager.d.ts.map +1 -1
- package/lib/typescript/module/cacheSignature.d.ts +42 -0
- package/lib/typescript/module/cacheSignature.d.ts.map +1 -0
- package/lib/typescript/module/config.d.ts +62 -1
- package/lib/typescript/module/config.d.ts.map +1 -1
- package/lib/typescript/module/hooks.d.ts +27 -5
- package/lib/typescript/module/hooks.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +20 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +20 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/tier.d.ts +55 -0
- package/lib/typescript/module/tier.d.ts.map +1 -0
- package/lib/typescript/module/types.d.ts +44 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/weekendPro.d.ts +30 -7
- package/lib/typescript/module/weekendPro.d.ts.map +1 -1
- package/package.json +7 -4
|
@@ -1,608 +1,15 @@
|
|
|
1
|
-
"use strict"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* LicenseManager - Singleton for managing React Buoy Pro licenses
|
|
1
|
+
"use strict";/*!
|
|
2
|
+
* PROPRIETARY — Buoy Pro license enforcement. NOT open source.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let
|
|
18
|
-
let _licenseStorage = null;
|
|
19
|
-
const _memoryStore = new Map();
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Load expo-file-system module
|
|
23
|
-
*/
|
|
24
|
-
function loadFileSystem() {
|
|
25
|
-
if (_FileSystem) return _FileSystem;
|
|
26
|
-
|
|
27
|
-
// Try legacy import first (v19+)
|
|
28
|
-
try {
|
|
29
|
-
const mod = require("expo-file-system/legacy");
|
|
30
|
-
_FileSystem = mod?.default || mod;
|
|
31
|
-
if (_FileSystem?.documentDirectory) {
|
|
32
|
-
return _FileSystem;
|
|
33
|
-
}
|
|
34
|
-
_FileSystem = null;
|
|
35
|
-
} catch {
|
|
36
|
-
// Not available
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Fall back to main import
|
|
40
|
-
try {
|
|
41
|
-
const mod = require("expo-file-system");
|
|
42
|
-
_FileSystem = mod?.default || mod;
|
|
43
|
-
if (_FileSystem?.documentDirectory) {
|
|
44
|
-
return _FileSystem;
|
|
45
|
-
}
|
|
46
|
-
_FileSystem = null;
|
|
47
|
-
} catch {
|
|
48
|
-
// Not available
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Get the protected license storage directory path
|
|
55
|
-
*/
|
|
56
|
-
function getLicenseStorageDir() {
|
|
57
|
-
const fs = loadFileSystem();
|
|
58
|
-
if (!fs?.documentDirectory) return null;
|
|
59
|
-
return `${fs.documentDirectory}${LICENSE_STORAGE_DIR}/`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get file path for a license storage key
|
|
64
|
-
*/
|
|
65
|
-
function getLicenseFilePath(key) {
|
|
66
|
-
const dir = getLicenseStorageDir();
|
|
67
|
-
if (!dir) return null;
|
|
68
|
-
return `${dir}${encodeURIComponent(key)}.json`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get protected license storage (file system based, separate from devtools)
|
|
73
|
-
* This storage survives app cache clears AND devtools "Clear Settings"
|
|
74
|
-
*/
|
|
75
|
-
async function getLicenseStorage() {
|
|
76
|
-
if (_licenseStorage) return _licenseStorage;
|
|
77
|
-
const fs = loadFileSystem();
|
|
78
|
-
if (fs?.documentDirectory) {
|
|
79
|
-
// Use file system storage in protected directory
|
|
80
|
-
_licenseStorage = {
|
|
81
|
-
async getItem(key) {
|
|
82
|
-
const filePath = getLicenseFilePath(key);
|
|
83
|
-
if (!filePath) return null;
|
|
84
|
-
try {
|
|
85
|
-
const fileInfo = await fs.getInfoAsync(filePath);
|
|
86
|
-
if (!fileInfo.exists) return null;
|
|
87
|
-
return await fs.readAsStringAsync(filePath);
|
|
88
|
-
} catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
async setItem(key, value) {
|
|
93
|
-
const storageDir = getLicenseStorageDir();
|
|
94
|
-
const filePath = getLicenseFilePath(key);
|
|
95
|
-
if (!storageDir || !filePath) return;
|
|
96
|
-
try {
|
|
97
|
-
// Ensure directory exists
|
|
98
|
-
const dirInfo = await fs.getInfoAsync(storageDir);
|
|
99
|
-
if (!dirInfo.exists) {
|
|
100
|
-
await fs.makeDirectoryAsync(storageDir, {
|
|
101
|
-
intermediates: true
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
await fs.writeAsStringAsync(filePath, value);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.warn("[Buoy] License storage write failed:", error);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
async removeItem(key) {
|
|
110
|
-
const filePath = getLicenseFilePath(key);
|
|
111
|
-
if (!filePath) return;
|
|
112
|
-
try {
|
|
113
|
-
await fs.deleteAsync(filePath, {
|
|
114
|
-
idempotent: true
|
|
115
|
-
});
|
|
116
|
-
} catch {
|
|
117
|
-
// Ignore
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
} else {
|
|
122
|
-
// Fallback to memory storage
|
|
123
|
-
_licenseStorage = {
|
|
124
|
-
async getItem(key) {
|
|
125
|
-
return _memoryStore.get(key) ?? null;
|
|
126
|
-
},
|
|
127
|
-
async setItem(key, value) {
|
|
128
|
-
_memoryStore.set(key, value);
|
|
129
|
-
},
|
|
130
|
-
async removeItem(key) {
|
|
131
|
-
_memoryStore.delete(key);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
return _licenseStorage;
|
|
136
|
-
}
|
|
137
|
-
import { LICENSE_STORAGE_KEY, LICENSE_CACHE_DURATION, LICENSE_REVALIDATION_INTERVAL } from "./config.js";
|
|
138
|
-
import { validateLicense, activateMachine, parseValidationResult, isFatalLicenseError } from "./api.js";
|
|
139
|
-
import { generateUUID, getDeviceMetadata, getDefaultDeviceName } from "./fingerprint";
|
|
140
|
-
|
|
141
|
-
/** Info passed to device registration callback */
|
|
142
|
-
|
|
143
|
-
/** Callback type for device registration events */
|
|
144
|
-
|
|
145
|
-
/** Pending registration info for async completion */
|
|
146
|
-
|
|
147
|
-
class LicenseManagerImpl {
|
|
148
|
-
state = {
|
|
149
|
-
isInitialized: false,
|
|
150
|
-
isValidating: false,
|
|
151
|
-
isPro: false,
|
|
152
|
-
licenseKey: null,
|
|
153
|
-
error: null,
|
|
154
|
-
expiresAt: null
|
|
155
|
-
};
|
|
156
|
-
listeners = new Set();
|
|
157
|
-
revalidationTimer = null;
|
|
158
|
-
cachedLicense = null;
|
|
159
|
-
|
|
160
|
-
// Promise for initialization to prevent double-init race condition
|
|
161
|
-
initPromise = null;
|
|
162
|
-
|
|
163
|
-
// Device registration callback for UI integration
|
|
164
|
-
deviceRegistrationCallback = null;
|
|
165
|
-
pendingRegistration = null;
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Initialize the license manager and load cached license
|
|
169
|
-
*/
|
|
170
|
-
async initialize() {
|
|
171
|
-
// Return existing promise if already initializing
|
|
172
|
-
if (this.initPromise) return this.initPromise;
|
|
173
|
-
if (this.state.isInitialized) return;
|
|
174
|
-
|
|
175
|
-
// Store the promise so concurrent calls wait for the same initialization
|
|
176
|
-
this.initPromise = this._doInitialize();
|
|
177
|
-
return this.initPromise;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Internal initialization logic
|
|
182
|
-
*/
|
|
183
|
-
async _doInitialize() {
|
|
184
|
-
if (this.state.isInitialized) return;
|
|
185
|
-
try {
|
|
186
|
-
// Load cached license from storage
|
|
187
|
-
const cached = await this.loadCachedLicense();
|
|
188
|
-
if (cached) {
|
|
189
|
-
this.cachedLicense = cached;
|
|
190
|
-
this.state.licenseKey = cached.licenseKey;
|
|
191
|
-
this.state.isPro = cached.isPro && this.isCacheValid(cached);
|
|
192
|
-
this.state.expiresAt = cached.expiresAt ? new Date(cached.expiresAt) : null;
|
|
193
|
-
|
|
194
|
-
// If we have a cached license, validate in background
|
|
195
|
-
if (cached.licenseKey) {
|
|
196
|
-
this.validateInBackground(cached.licenseKey);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
this.state.isInitialized = true;
|
|
200
|
-
this.emit({
|
|
201
|
-
type: "initialized",
|
|
202
|
-
isPro: this.state.isPro
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Start periodic revalidation (once per day for usage analytics)
|
|
206
|
-
this.startRevalidationTimer();
|
|
207
|
-
} catch (error) {
|
|
208
|
-
console.warn("[React Buoy] License initialization failed:", error);
|
|
209
|
-
this.state.isInitialized = true;
|
|
210
|
-
this.emit({
|
|
211
|
-
type: "initialized",
|
|
212
|
-
isPro: false
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Set and validate a license key.
|
|
219
|
-
*
|
|
220
|
-
* Simple validation - just checks if the key is valid.
|
|
221
|
-
* No device registration or limits - we trust our customers.
|
|
222
|
-
* Keygen tracks validation calls for usage analytics.
|
|
223
|
-
*/
|
|
224
|
-
async setLicenseKey(licenseKey) {
|
|
225
|
-
if (!licenseKey || licenseKey.trim().length === 0) {
|
|
226
|
-
this.state.error = "License key is required";
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Normalize license key
|
|
231
|
-
const normalizedKey = licenseKey.trim().toUpperCase();
|
|
232
|
-
this.state.licenseKey = normalizedKey;
|
|
233
|
-
this.state.isValidating = true;
|
|
234
|
-
this.state.error = null;
|
|
235
|
-
this.emit({
|
|
236
|
-
type: "validating",
|
|
237
|
-
isPro: this.state.isPro
|
|
238
|
-
});
|
|
239
|
-
try {
|
|
240
|
-
// Validate the license (just check if key is valid)
|
|
241
|
-
const response = await validateLicense(normalizedKey);
|
|
242
|
-
const result = parseValidationResult(response);
|
|
243
|
-
|
|
244
|
-
// Check for fatal errors (expired, suspended, not found, etc.)
|
|
245
|
-
if (isFatalLicenseError(result.code)) {
|
|
246
|
-
this.state.isValidating = false;
|
|
247
|
-
this.state.error = result.detail;
|
|
248
|
-
await this.clearCachedLicense();
|
|
249
|
-
this.emit({
|
|
250
|
-
type: "error",
|
|
251
|
-
isPro: false,
|
|
252
|
-
error: result.detail
|
|
253
|
-
});
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// If response.data is null, the license is invalid
|
|
258
|
-
if (!response.data) {
|
|
259
|
-
this.state.isValidating = false;
|
|
260
|
-
this.state.error = result.detail || "License not found";
|
|
261
|
-
await this.clearCachedLicense();
|
|
262
|
-
this.emit({
|
|
263
|
-
type: "error",
|
|
264
|
-
isPro: false,
|
|
265
|
-
error: this.state.error
|
|
266
|
-
});
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// License is valid!
|
|
271
|
-
this.state.isPro = true;
|
|
272
|
-
this.state.isValidating = false;
|
|
273
|
-
this.state.expiresAt = response.data.attributes.expiry ? new Date(response.data.attributes.expiry) : null;
|
|
274
|
-
const licenseId = response.data.id;
|
|
275
|
-
const now = Date.now();
|
|
276
|
-
|
|
277
|
-
// Get or generate fingerprint for this device
|
|
278
|
-
let fingerprint = this.cachedLicense?.fingerprint;
|
|
279
|
-
let deviceName = this.cachedLicense?.deviceName;
|
|
280
|
-
let needsRegistration = false;
|
|
281
|
-
if (!fingerprint) {
|
|
282
|
-
// First time on this device - generate fingerprint and register
|
|
283
|
-
fingerprint = generateUUID();
|
|
284
|
-
deviceName = getDefaultDeviceName();
|
|
285
|
-
needsRegistration = true;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Only register if this is a new device (no cached fingerprint)
|
|
289
|
-
let deferCaching = false;
|
|
290
|
-
if (needsRegistration) {
|
|
291
|
-
if (this.deviceRegistrationCallback) {
|
|
292
|
-
// UI callback registered - defer to UI for device naming
|
|
293
|
-
// Don't cache fingerprint yet - wait for completeDeviceRegistration
|
|
294
|
-
this.pendingRegistration = {
|
|
295
|
-
licenseId,
|
|
296
|
-
licenseKey: normalizedKey,
|
|
297
|
-
fingerprint
|
|
298
|
-
};
|
|
299
|
-
this.deviceRegistrationCallback({
|
|
300
|
-
fingerprint,
|
|
301
|
-
defaultName: deviceName
|
|
302
|
-
});
|
|
303
|
-
deferCaching = true;
|
|
304
|
-
} else {
|
|
305
|
-
// No UI callback - register immediately with default name
|
|
306
|
-
this.registerDeviceAsync(licenseId, normalizedKey, fingerprint, deviceName);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Cache the validation result
|
|
311
|
-
const storage = await getLicenseStorage();
|
|
312
|
-
const cached = {
|
|
313
|
-
licenseKey: normalizedKey,
|
|
314
|
-
licenseId,
|
|
315
|
-
valid: true,
|
|
316
|
-
isPro: true,
|
|
317
|
-
expiresAt: response.data.attributes.expiry ?? null,
|
|
318
|
-
cachedAt: now,
|
|
319
|
-
lastValidatedAt: now,
|
|
320
|
-
// Don't cache fingerprint if we're deferring to UI for registration
|
|
321
|
-
fingerprint: deferCaching ? undefined : fingerprint,
|
|
322
|
-
deviceName: deferCaching ? undefined : deviceName
|
|
323
|
-
};
|
|
324
|
-
await storage.setItem(LICENSE_STORAGE_KEY, JSON.stringify(cached));
|
|
325
|
-
this.cachedLicense = cached;
|
|
326
|
-
this.emit({
|
|
327
|
-
type: "validated",
|
|
328
|
-
isPro: true
|
|
329
|
-
});
|
|
330
|
-
return true;
|
|
331
|
-
} catch (error) {
|
|
332
|
-
const errorMessage = error instanceof Error ? error.message : "Validation failed";
|
|
333
|
-
|
|
334
|
-
// Check for network error and use cache
|
|
335
|
-
if (this.isNetworkError(error) && this.cachedLicense && this.isCacheValid(this.cachedLicense)) {
|
|
336
|
-
this.state.isPro = this.cachedLicense.isPro;
|
|
337
|
-
this.state.isValidating = false;
|
|
338
|
-
this.emit({
|
|
339
|
-
type: "validated",
|
|
340
|
-
isPro: this.state.isPro
|
|
341
|
-
});
|
|
342
|
-
return this.state.isPro;
|
|
343
|
-
}
|
|
344
|
-
this.state.isValidating = false;
|
|
345
|
-
this.state.error = errorMessage;
|
|
346
|
-
this.emit({
|
|
347
|
-
type: "error",
|
|
348
|
-
isPro: false,
|
|
349
|
-
error: errorMessage
|
|
350
|
-
});
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Check if an error is a network error (not an API validation error)
|
|
357
|
-
*/
|
|
358
|
-
isNetworkError(error) {
|
|
359
|
-
if (error instanceof TypeError) {
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
if (error instanceof Error) {
|
|
363
|
-
const message = error.message.toLowerCase();
|
|
364
|
-
return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("connection") || message.includes("offline") || message.includes("failed to fetch");
|
|
365
|
-
}
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Clear the current license
|
|
371
|
-
*/
|
|
372
|
-
async clearLicense() {
|
|
373
|
-
this.state.licenseKey = null;
|
|
374
|
-
this.state.isPro = false;
|
|
375
|
-
this.state.expiresAt = null;
|
|
376
|
-
this.state.error = null;
|
|
377
|
-
this.cachedLicense = null;
|
|
378
|
-
await this.clearCachedLicense();
|
|
379
|
-
this.emit({
|
|
380
|
-
type: "deactivated",
|
|
381
|
-
isPro: false
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Check if the user has Pro features
|
|
387
|
-
*/
|
|
388
|
-
isPro() {
|
|
389
|
-
return this.state.isPro;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Check if a specific entitlement is available
|
|
394
|
-
*/
|
|
395
|
-
hasEntitlement(code) {
|
|
396
|
-
if (code === "PRO") {
|
|
397
|
-
return this.state.isPro;
|
|
398
|
-
}
|
|
399
|
-
return this.state.isPro;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Get current license state
|
|
404
|
-
*/
|
|
405
|
-
getState() {
|
|
406
|
-
return {
|
|
407
|
-
...this.state
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Subscribe to license events
|
|
413
|
-
*/
|
|
414
|
-
subscribe(listener) {
|
|
415
|
-
this.listeners.add(listener);
|
|
416
|
-
return () => this.listeners.delete(listener);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Subscribe to device registration events
|
|
421
|
-
* When a new device needs to be registered, the callback is invoked
|
|
422
|
-
* with the fingerprint and default name suggestion.
|
|
423
|
-
* Call completeDeviceRegistration() to finish registration with user's chosen name.
|
|
424
|
-
*/
|
|
425
|
-
onDeviceRegistrationNeeded(callback) {
|
|
426
|
-
this.deviceRegistrationCallback = callback;
|
|
427
|
-
return () => {
|
|
428
|
-
this.deviceRegistrationCallback = null;
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Complete device registration with user-provided name
|
|
434
|
-
* Call this after onDeviceRegistrationNeeded callback to finish registration
|
|
435
|
-
*/
|
|
436
|
-
async completeDeviceRegistration(deviceName) {
|
|
437
|
-
if (!this.pendingRegistration) {
|
|
438
|
-
console.warn("[Buoy] No pending device registration");
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const {
|
|
442
|
-
licenseId,
|
|
443
|
-
licenseKey,
|
|
444
|
-
fingerprint
|
|
445
|
-
} = this.pendingRegistration;
|
|
446
|
-
const name = deviceName.trim() || getDefaultDeviceName();
|
|
447
|
-
|
|
448
|
-
// Register the device and wait for completion
|
|
449
|
-
const metadata = getDeviceMetadata();
|
|
450
|
-
try {
|
|
451
|
-
await activateMachine(licenseId, licenseKey, fingerprint, name, metadata);
|
|
452
|
-
} catch (error) {
|
|
453
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
454
|
-
// Handle expected "errors" that aren't really errors
|
|
455
|
-
if (errorMessage === "FINGERPRINT_TAKEN" || errorMessage === "MACHINE_LIMIT_EXCEEDED") {
|
|
456
|
-
// Device already registered or limit exceeded but we're not enforcing - continue
|
|
457
|
-
} else {
|
|
458
|
-
console.warn("[Buoy] Device registration failed:", errorMessage);
|
|
459
|
-
throw error; // Re-throw unexpected errors
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Update cached license with device info
|
|
464
|
-
if (this.cachedLicense) {
|
|
465
|
-
this.cachedLicense.fingerprint = fingerprint;
|
|
466
|
-
this.cachedLicense.deviceName = name;
|
|
467
|
-
const storage = await getLicenseStorage();
|
|
468
|
-
await storage.setItem(LICENSE_STORAGE_KEY, JSON.stringify(this.cachedLicense));
|
|
469
|
-
}
|
|
470
|
-
this.pendingRegistration = null;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Private methods
|
|
474
|
-
|
|
475
|
-
emit(event) {
|
|
476
|
-
// Iterate over a copy to prevent issues if listeners modify the Set
|
|
477
|
-
const listeners = [...this.listeners];
|
|
478
|
-
listeners.forEach(listener => {
|
|
479
|
-
try {
|
|
480
|
-
listener(event);
|
|
481
|
-
} catch (error) {
|
|
482
|
-
console.warn("[React Buoy] License listener error:", error);
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
isCacheValid(cached) {
|
|
487
|
-
const now = Date.now();
|
|
488
|
-
const cacheAge = now - cached.cachedAt;
|
|
489
|
-
if (cacheAge > LICENSE_CACHE_DURATION) {
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
if (cached.expiresAt) {
|
|
493
|
-
const expiry = new Date(cached.expiresAt).getTime();
|
|
494
|
-
if (now > expiry) {
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
async loadCachedLicense() {
|
|
501
|
-
try {
|
|
502
|
-
const storage = await getLicenseStorage();
|
|
503
|
-
const data = await storage.getItem(LICENSE_STORAGE_KEY);
|
|
504
|
-
if (!data) return null;
|
|
505
|
-
const cached = JSON.parse(data);
|
|
506
|
-
return cached;
|
|
507
|
-
} catch (error) {
|
|
508
|
-
console.warn("[Buoy] Error loading cached license:", error);
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
async clearCachedLicense() {
|
|
513
|
-
this.cachedLicense = null;
|
|
514
|
-
try {
|
|
515
|
-
const storage = await getLicenseStorage();
|
|
516
|
-
await storage.removeItem(LICENSE_STORAGE_KEY);
|
|
517
|
-
} catch {
|
|
518
|
-
// Ignore
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Register device in background for visibility (non-blocking)
|
|
524
|
-
* Failures don't affect Pro status - this is just for analytics
|
|
525
|
-
*/
|
|
526
|
-
registerDeviceAsync(licenseId, licenseKey, fingerprint, deviceName) {
|
|
527
|
-
const metadata = getDeviceMetadata();
|
|
528
|
-
activateMachine(licenseId, licenseKey, fingerprint, deviceName, metadata).catch(error => {
|
|
529
|
-
// Don't block on registration failures - user still gets Pro
|
|
530
|
-
if (error.message !== "FINGERPRINT_TAKEN" && error.message !== "MACHINE_LIMIT_EXCEEDED") {
|
|
531
|
-
console.warn("[Buoy] Device registration failed (non-blocking):", error.message);
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
validateInBackground(licenseKey) {
|
|
536
|
-
validateLicense(licenseKey).then(async response => {
|
|
537
|
-
const result = parseValidationResult(response);
|
|
538
|
-
if (result.valid && response.data) {
|
|
539
|
-
this.state.isPro = true;
|
|
540
|
-
this.emit({
|
|
541
|
-
type: "validated",
|
|
542
|
-
isPro: true
|
|
543
|
-
});
|
|
544
|
-
} else if (isFatalLicenseError(result.code)) {
|
|
545
|
-
// License is truly invalid - clear everything
|
|
546
|
-
console.warn("[React Buoy] Background validation: license is invalid -", result.code);
|
|
547
|
-
this.state.isPro = false;
|
|
548
|
-
await this.clearCachedLicense();
|
|
549
|
-
this.emit({
|
|
550
|
-
type: "deactivated",
|
|
551
|
-
isPro: false
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
}).catch(error => {
|
|
555
|
-
// Network errors - don't clear anything, just log
|
|
556
|
-
console.warn("[React Buoy] Background validation failed:", error);
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
startRevalidationTimer() {
|
|
560
|
-
if (this.revalidationTimer) {
|
|
561
|
-
clearInterval(this.revalidationTimer);
|
|
562
|
-
}
|
|
563
|
-
this.revalidationTimer = setInterval(() => {
|
|
564
|
-
if (this.state.licenseKey && !this.state.isValidating) {
|
|
565
|
-
this.validateInBackground(this.state.licenseKey);
|
|
566
|
-
}
|
|
567
|
-
}, LICENSE_REVALIDATION_INTERVAL);
|
|
568
|
-
|
|
569
|
-
// Store for hot reload cleanup
|
|
570
|
-
if (__DEV__) {
|
|
571
|
-
global.__LICENSE_MANAGER_TIMER__ = this.revalidationTimer;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Stop the revalidation timer
|
|
577
|
-
*/
|
|
578
|
-
stopRevalidationTimer() {
|
|
579
|
-
if (this.revalidationTimer) {
|
|
580
|
-
clearInterval(this.revalidationTimer);
|
|
581
|
-
this.revalidationTimer = null;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Force re-registration by clearing cached fingerprint
|
|
587
|
-
* (For debugging/testing - forces device to re-register on next validation)
|
|
588
|
-
*/
|
|
589
|
-
async forceReregister() {
|
|
590
|
-
if (this.cachedLicense) {
|
|
591
|
-
this.cachedLicense.fingerprint = undefined;
|
|
592
|
-
this.cachedLicense.deviceName = undefined;
|
|
593
|
-
}
|
|
594
|
-
await this.clearCachedLicense();
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Singleton instance
|
|
599
|
-
export const LicenseManager = new LicenseManagerImpl();
|
|
600
|
-
|
|
601
|
-
// Hot reload cleanup - prevent timer leaks during development
|
|
602
|
-
|
|
603
|
-
if (__DEV__) {
|
|
604
|
-
// Clear any existing timer from previous hot reload
|
|
605
|
-
if (global.__LICENSE_MANAGER_TIMER__) {
|
|
606
|
-
clearInterval(global.__LICENSE_MANAGER_TIMER__);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
4
|
+
* Copyright (c) Buoy. All rights reserved. Licensed under the Buoy EULA
|
|
5
|
+
* (https://buoy.gg/terms), not an OSS license.
|
|
6
|
+
*
|
|
7
|
+
* This file is the technological measure that controls access to paid Buoy Pro
|
|
8
|
+
* features. Modifying, patching, aliasing, stubbing, or otherwise altering it
|
|
9
|
+
* so that Pro features unlock without a valid license is a breach of the EULA
|
|
10
|
+
* and, in the United States, circumvention of an access control under
|
|
11
|
+
* 17 U.S.C. § 1201 — which applies whether or not any code is copied.
|
|
12
|
+
*
|
|
13
|
+
* Pro is $9/mo, and is free for everyone every weekend. Buy a key at
|
|
14
|
+
* https://buoy.gg or use it free on Saturday and Sunday.
|
|
15
|
+
*/const N="buoy-license";let c=null;let d=null;const f=new Map;function I(){if(c)return c;try{const a=require("expo-file-system/legacy");c=a?.default||a;if(c?.documentDirectory){return c}c=null}catch{}try{const a=require("expo-file-system");c=a?.default||a;if(c?.documentDirectory){return c}c=null}catch{}return null}function v(){const a=I();if(!a?.documentDirectory)return null;return`${a.documentDirectory}${N}/`}function y(a){const e=v();if(!e)return null;return`${e}${encodeURIComponent(a)}.json`}async function m(){if(d)return d;const a=I();if(a?.documentDirectory){d={async getItem(e){const t=y(e);if(!t)return null;try{const i=await a.getInfoAsync(t);if(!i.exists)return null;return await a.readAsStringAsync(t)}catch{return null}},async setItem(e,t){const i=v();const s=y(e);if(!i||!s)return;try{const r=await a.getInfoAsync(i);if(!r.exists){await a.makeDirectoryAsync(i,{intermediates:true})}await a.writeAsStringAsync(s,t)}catch(r){console.warn("[Buoy] License storage write failed:",r)}},async removeItem(e){const t=y(e);if(!t)return;try{await a.deleteAsync(t,{idempotent:true})}catch{}}}}else{d={async getItem(e){return f.get(e)??null},async setItem(e,t){f.set(e,t)},async removeItem(e){f.delete(e)}}}return d}import{LICENSE_STORAGE_KEY as g,LICENSE_CACHE_DURATION as S,LICENSE_REVALIDATION_INTERVAL as x}from"./config.js";import{validateLicense as L,activateMachine as E,parseValidationResult as w,isFatalLicenseError as A}from"./api.js";import{generateUUID as V,getDeviceMetadata as _,getDefaultDeviceName as C}from"./fingerprint";import{signCache as b,verifyCacheSignature as k}from"./cacheSignature.js";import{resolveTier as R,tierAfterFatalError as T}from"./tier.js";function P(a){if(a.tier)return a.tier;return a.isPro?"pro":"anonymous"}class M{state={isInitialized:false,isValidating:false,isPro:false,tier:"anonymous",tierSource:"none",licenseKey:null,error:null,expiresAt:null};listeners=new Set;revalidationTimer=null;cachedLicense=null;initPromise=null;deviceRegistrationCallback=null;pendingRegistration=null;async initialize(){if(this.initPromise)return this.initPromise;if(this.state.isInitialized)return;this.initPromise=this._doInitialize();return this.initPromise}async _doInitialize(){if(this.state.isInitialized)return;try{const e=await this.loadCachedLicense();if(e){const t=k(e);this.cachedLicense=e;this.state.licenseKey=e.licenseKey;this.applyTier(t&&this.isCacheValid(e)?P(e):"anonymous");this.state.expiresAt=e.expiresAt?new Date(e.expiresAt):null;if(e.licenseKey){this.validateInBackground(e.licenseKey)}}this.state.isInitialized=true;this.emit({type:"initialized",isPro:this.state.isPro});this.startRevalidationTimer()}catch(e){console.warn("[React Buoy] License initialization failed:",e);this.state.isInitialized=true;this.emit({type:"initialized",isPro:false})}}async setLicenseKey(e){if(!e||e.trim().length===0){this.state.error="License key is required";return false}const t=e.trim().toUpperCase();this.state.licenseKey=t;this.state.isValidating=true;this.state.error=null;this.emit({type:"validating",isPro:this.state.isPro});try{const i=await L(t);const s=w(i);if(A(s.code)){this.applyTier(T(s.code));this.state.isValidating=false;this.state.error=s.detail;await this.clearCachedLicense();this.emit({type:"error",isPro:false,error:s.detail});return false}if(!i.data){this.state.isValidating=false;this.state.error=s.detail||"License not found";await this.clearCachedLicense();this.emit({type:"error",isPro:false,error:this.state.error});return false}const r=R(i.data);this.applyTier(r);this.state.isValidating=false;this.state.expiresAt=i.data.attributes.expiry?new Date(i.data.attributes.expiry):null;const o=i.data.id;const l=Date.now();let n=this.cachedLicense?.fingerprint;let h=this.cachedLicense?.deviceName;let p=false;if(!n){n=V();h=C();p=true}let u=false;if(p){if(this.deviceRegistrationCallback){this.pendingRegistration={licenseId:o,licenseKey:t,fingerprint:n};this.deviceRegistrationCallback({fingerprint:n,defaultName:h});u=true}else{this.registerDeviceAsync(o,t,n,h)}}const D={licenseKey:t,licenseId:o,valid:true,isPro:r==="pro",tier:r,expiresAt:i.data.attributes.expiry??null,cachedAt:l,lastValidatedAt:l,fingerprint:u?void 0:n,deviceName:u?void 0:h};this.cachedLicense=await this.persistCache(D);this.emit({type:"validated",isPro:this.state.isPro});return true}catch(i){const s=i instanceof Error?i.message:"Validation failed";if(this.isNetworkError(i)&&this.cachedLicense&&this.isCacheValid(this.cachedLicense)){this.applyTier(P(this.cachedLicense));this.state.isValidating=false;this.emit({type:"validated",isPro:this.state.isPro});return true}this.state.isValidating=false;this.state.error=s;this.emit({type:"error",isPro:false,error:s});return false}}isNetworkError(e){if(e instanceof TypeError){return true}if(e instanceof Error){const t=e.message.toLowerCase();return t.includes("network")||t.includes("fetch")||t.includes("timeout")||t.includes("connection")||t.includes("offline")||t.includes("failed to fetch")}return false}async clearLicense(){this.state.licenseKey=null;this.applyTier("anonymous");this.state.expiresAt=null;this.state.error=null;this.cachedLicense=null;await this.clearCachedLicense();this.emit({type:"deactivated",isPro:false})}applyTier(e){this.state.tier=e;this.state.tierSource=e==="anonymous"?"none":"key";this.state.isPro=e==="pro"}getTier(){return this.state.tier}isPro(){return this.state.tier==="pro"}hasEntitlement(e){if(e==="PRO"){return this.state.isPro}return this.state.isPro}getState(){return{...this.state}}subscribe(e){this.listeners.add(e);return()=>this.listeners.delete(e)}onDeviceRegistrationNeeded(e){this.deviceRegistrationCallback=e;return()=>{this.deviceRegistrationCallback=null}}async completeDeviceRegistration(e){if(!this.pendingRegistration){console.warn("[Buoy] No pending device registration");return}const{licenseId:t,licenseKey:i,fingerprint:s}=this.pendingRegistration;const r=e.trim()||C();const o=_();try{await E(t,i,s,r,o)}catch(l){const n=l instanceof Error?l.message:String(l);if(n==="FINGERPRINT_TAKEN"||n==="MACHINE_LIMIT_EXCEEDED"){}else{console.warn("[Buoy] Device registration failed:",n);throw l}}if(this.cachedLicense){this.cachedLicense.fingerprint=s;this.cachedLicense.deviceName=r;this.cachedLicense=await this.persistCache(this.cachedLicense)}this.pendingRegistration=null}emit(e){const t=[...this.listeners];t.forEach(i=>{try{i(e)}catch(s){console.warn("[React Buoy] License listener error:",s)}})}isCacheValid(e){const t=Date.now();const i=t-e.cachedAt;if(i>S){return false}if(e.expiresAt){const s=new Date(e.expiresAt).getTime();if(t>s){return false}}return true}async persistCache(e){const t=b(e);const i=await m();await i.setItem(g,JSON.stringify(t));return t}async loadCachedLicense(){try{const e=await m();const t=await e.getItem(g);if(!t)return null;const i=JSON.parse(t);return i}catch(e){console.warn("[Buoy] Error loading cached license:",e);return null}}async clearCachedLicense(){this.cachedLicense=null;try{const e=await m();await e.removeItem(g)}catch{}}registerDeviceAsync(e,t,i,s){const r=_();E(e,t,i,s,r).catch(o=>{if(o.message!=="FINGERPRINT_TAKEN"&&o.message!=="MACHINE_LIMIT_EXCEEDED"){console.warn("[Buoy] Device registration failed (non-blocking):",o.message)}})}validateInBackground(e){L(e).then(async t=>{const i=w(t);if(i.valid&&t.data){const s=R(t.data);this.applyTier(s);const r=Date.now();this.cachedLicense=await this.persistCache({...this.cachedLicense??{},licenseKey:e,licenseId:t.data.id,valid:true,isPro:s==="pro",tier:s,expiresAt:t.data.attributes.expiry??null,cachedAt:r,lastValidatedAt:r});this.state.expiresAt=t.data.attributes.expiry?new Date(t.data.attributes.expiry):null;this.emit({type:"validated",isPro:this.state.isPro})}else if(A(i.code)){console.warn("[React Buoy] Background validation: license is invalid -",i.code);this.applyTier(T(i.code));await this.clearCachedLicense();this.emit({type:"deactivated",isPro:false})}}).catch(t=>{console.warn("[React Buoy] Background validation failed:",t)})}startRevalidationTimer(){if(this.revalidationTimer){clearInterval(this.revalidationTimer)}this.revalidationTimer=setInterval(()=>{if(this.state.licenseKey&&!this.state.isValidating){this.validateInBackground(this.state.licenseKey)}},x);if(__DEV__){global.__LICENSE_MANAGER_TIMER__=this.revalidationTimer}}stopRevalidationTimer(){if(this.revalidationTimer){clearInterval(this.revalidationTimer);this.revalidationTimer=null}}async forceReregister(){if(this.cachedLicense){this.cachedLicense.fingerprint=void 0;this.cachedLicense.deviceName=void 0}await this.clearCachedLicense()}}export const LicenseManager=new M;if(__DEV__){if(global.__LICENSE_MANAGER_TIMER__){clearInterval(global.__LICENSE_MANAGER_TIMER__)}}
|