@buoy-gg/license 7.0.41 → 7.0.42
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/DeviceRegistrationModal.js +1 -1
- package/lib/commonjs/LicenseManager.js +16 -16
- package/lib/commonjs/accountAccess.js +1 -1
- package/lib/commonjs/api.js +1 -1
- package/lib/commonjs/cacheSignature.js +15 -15
- package/lib/commonjs/config.js +16 -16
- package/lib/commonjs/freeToken.js +5 -5
- package/lib/commonjs/hooks.js +16 -16
- package/lib/commonjs/index.js +2 -2
- package/lib/commonjs/index.web.js +1 -1
- package/lib/commonjs/realNow.js +1 -0
- package/lib/commonjs/storage.js +1 -1
- package/lib/commonjs/tier.js +16 -16
- package/lib/commonjs/weekendPro.js +18 -18
- package/lib/module/DeviceRegistrationModal.js +1 -1
- package/lib/module/LicenseManager.js +15 -15
- package/lib/module/accountAccess.js +1 -1
- package/lib/module/api.js +1 -1
- package/lib/module/cacheSignature.js +15 -15
- package/lib/module/config.js +16 -16
- package/lib/module/device-info.web.js +1 -1
- package/lib/module/freeToken.js +5 -5
- package/lib/module/hooks.js +16 -16
- package/lib/module/index.js +17 -17
- package/lib/module/index.web.js +17 -17
- package/lib/module/realNow.js +1 -0
- package/lib/module/storage.js +1 -1
- package/lib/module/storage.web.js +1 -1
- package/lib/module/tier.js +16 -16
- package/lib/module/weekendPro.js +17 -17
- package/lib/typescript/commonjs/realNow.d.ts +11 -0
- package/lib/typescript/module/realNow.d.ts +11 -0
- package/lib/web/chunks/LicenseManager-7D2GGBCB.mjs +1 -0
- package/lib/web/chunks/chunk-ZSTLYSNF.mjs +68 -0
- package/lib/web/index.mjs +3 -7
- package/package.json +1 -1
- package/lib/typescript/commonjs/DeviceRegistrationModal.d.ts.map +0 -1
- package/lib/typescript/commonjs/LicenseManager.d.ts.map +0 -1
- package/lib/typescript/commonjs/accountAccess.d.ts.map +0 -1
- package/lib/typescript/commonjs/api.d.ts.map +0 -1
- package/lib/typescript/commonjs/cacheSignature.d.ts.map +0 -1
- package/lib/typescript/commonjs/config.d.ts.map +0 -1
- package/lib/typescript/commonjs/device-info.d.ts.map +0 -1
- package/lib/typescript/commonjs/device-info.web.d.ts.map +0 -1
- package/lib/typescript/commonjs/fingerprint.d.ts.map +0 -1
- package/lib/typescript/commonjs/fingerprint.web.d.ts.map +0 -1
- package/lib/typescript/commonjs/foreground.d.ts.map +0 -1
- package/lib/typescript/commonjs/foreground.web.d.ts.map +0 -1
- package/lib/typescript/commonjs/freeToken.d.ts.map +0 -1
- package/lib/typescript/commonjs/headless.d.ts.map +0 -1
- package/lib/typescript/commonjs/hooks.d.ts.map +0 -1
- package/lib/typescript/commonjs/index.d.ts.map +0 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +0 -1
- package/lib/typescript/commonjs/storage.d.ts.map +0 -1
- package/lib/typescript/commonjs/storage.web.d.ts.map +0 -1
- package/lib/typescript/commonjs/tier.d.ts.map +0 -1
- package/lib/typescript/commonjs/types.d.ts.map +0 -1
- package/lib/typescript/commonjs/weekendPro.d.ts.map +0 -1
- package/lib/typescript/module/DeviceRegistrationModal.d.ts.map +0 -1
- package/lib/typescript/module/LicenseManager.d.ts.map +0 -1
- package/lib/typescript/module/accountAccess.d.ts.map +0 -1
- package/lib/typescript/module/api.d.ts.map +0 -1
- package/lib/typescript/module/cacheSignature.d.ts.map +0 -1
- package/lib/typescript/module/config.d.ts.map +0 -1
- package/lib/typescript/module/device-info.d.ts.map +0 -1
- package/lib/typescript/module/device-info.web.d.ts.map +0 -1
- package/lib/typescript/module/fingerprint.d.ts.map +0 -1
- package/lib/typescript/module/fingerprint.web.d.ts.map +0 -1
- package/lib/typescript/module/foreground.d.ts.map +0 -1
- package/lib/typescript/module/foreground.web.d.ts.map +0 -1
- package/lib/typescript/module/freeToken.d.ts.map +0 -1
- package/lib/typescript/module/headless.d.ts.map +0 -1
- package/lib/typescript/module/hooks.d.ts.map +0 -1
- package/lib/typescript/module/index.d.ts.map +0 -1
- package/lib/typescript/module/index.web.d.ts.map +0 -1
- package/lib/typescript/module/storage.d.ts.map +0 -1
- package/lib/typescript/module/storage.web.d.ts.map +0 -1
- package/lib/typescript/module/tier.d.ts.map +0 -1
- package/lib/typescript/module/types.d.ts.map +0 -1
- package/lib/typescript/module/weekendPro.d.ts.map +0 -1
- package/lib/web/chunks/LicenseManager-AEFCYYMQ.mjs +0 -7
- package/lib/web/chunks/LicenseManager-JHLFWSUD.mjs +0 -2
- package/lib/web/chunks/LicenseManager-VWEJJHBH.mjs +0 -7
- package/lib/web/chunks/chunk-GRUXDUJG.mjs +0 -74
- package/lib/web/chunks/chunk-OPWF6JR6.mjs +0 -1480
- package/lib/web/chunks/chunk-TVOTAEQX.mjs +0 -1483
|
@@ -1,1480 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
// ../../scripts/web-environment.js
|
|
4
|
-
var __DEV__ = process.env.NODE_ENV !== "production";
|
|
5
|
-
|
|
6
|
-
// src/storage.web.ts
|
|
7
|
-
var memory = /* @__PURE__ */ new Map();
|
|
8
|
-
var storage = {
|
|
9
|
-
async getItem(key) {
|
|
10
|
-
try {
|
|
11
|
-
return localStorage.getItem(key) ?? memory.get(key) ?? null;
|
|
12
|
-
} catch {
|
|
13
|
-
return memory.get(key) ?? null;
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
async setItem(key, value) {
|
|
17
|
-
memory.set(key, value);
|
|
18
|
-
try {
|
|
19
|
-
localStorage.setItem(key, value);
|
|
20
|
-
} catch {
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
async removeItem(key) {
|
|
24
|
-
memory.delete(key);
|
|
25
|
-
try {
|
|
26
|
-
localStorage.removeItem(key);
|
|
27
|
-
} catch {
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
async function getLicenseStorage() {
|
|
32
|
-
return storage;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/config.ts
|
|
36
|
-
var KEYGEN_CONFIG = {
|
|
37
|
-
accountId: "b0eba0e8-e904-4906-b644-38f9aab57bed",
|
|
38
|
-
/**
|
|
39
|
-
* The Keygen PRODUCT id, used to check that a validated license belongs to
|
|
40
|
-
* Buoy (see `isAcceptedValidation`).
|
|
41
|
-
*
|
|
42
|
-
* Careful: this field previously held `1037431d-b2fe-48db-aca9-10c5e9c4d944`,
|
|
43
|
-
* which is not a product at all — it was the ID of a product token.
|
|
44
|
-
* Verified against the Keygen dashboard (Aug 2026): the account has exactly
|
|
45
|
-
* one product, "Buoy", and this is its id.
|
|
46
|
-
*/
|
|
47
|
-
productId: "bf67cb43-eeef-40ca-b308-e6e1b22c0eaa",
|
|
48
|
-
/**
|
|
49
|
-
* Buoy's license endpoint. It sits in front of Keygen and answers from a
|
|
50
|
-
* cache, so the license server hears about a key about once an hour no
|
|
51
|
-
* matter how many devices hold it; Keygen's response body comes back
|
|
52
|
-
* unchanged. Device registration goes through it too, which is why no
|
|
53
|
-
* product token ships in this package any more: the token used to be
|
|
54
|
-
* embedded here, and anything embedded in an app is public.
|
|
55
|
-
*/
|
|
56
|
-
apiUrl: "https://buoy.gg/api/license"
|
|
57
|
-
};
|
|
58
|
-
var FREE_TOKEN_PUBLIC_KEY = "3b546fc717a62683f3236c70167899a271bee3ffc04df6ec1eea60a394a2f1e9";
|
|
59
|
-
var KEYGEN_POLICIES = {
|
|
60
|
-
/** Free tier — issued automatically on first dashboard visit. */
|
|
61
|
-
free: "394e3351-1249-4f73-807a-703408604c0e",
|
|
62
|
-
/** Solo (sold as "Pro"; the id predates the Solo/Business split). */
|
|
63
|
-
solo: "fa5d3c8f-5379-40f1-b63e-ab8a974b2f28",
|
|
64
|
-
/** Business — an exact clone of the Solo policy. */
|
|
65
|
-
business: "690c2212-8e54-426f-b4b4-002ee1c0ae2b"
|
|
66
|
-
};
|
|
67
|
-
var LICENSE_CACHE_DURATION = 30 * 24 * 60 * 60 * 1e3;
|
|
68
|
-
var LICENSE_REVALIDATION_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
69
|
-
var LICENSE_STORAGE_KEY = "buoy-gg-license";
|
|
70
|
-
var DEVICE_FINGERPRINT_STORAGE_KEY = "buoy-gg-device-fingerprint";
|
|
71
|
-
var LICENSE_LAUNCH_VALIDATION_TTL = 60 * 60 * 1e3;
|
|
72
|
-
var LICENSE_TRANSIENT_RETRY_DELAYS = [
|
|
73
|
-
5 * 1e3,
|
|
74
|
-
20 * 1e3,
|
|
75
|
-
60 * 1e3,
|
|
76
|
-
5 * 60 * 1e3,
|
|
77
|
-
15 * 60 * 1e3
|
|
78
|
-
];
|
|
79
|
-
var LICENSE_RATE_LIMIT_DEFAULT_WAIT = 60 * 1e3;
|
|
80
|
-
var LICENSE_QUOTA_COOLDOWN = 60 * 60 * 1e3;
|
|
81
|
-
var LICENSE_VERDICT_CACHE_DURATION = 24 * 60 * 60 * 1e3;
|
|
82
|
-
var LICENSE_FLEET_MACHINE_THRESHOLD = 50;
|
|
83
|
-
var LICENSE_QUIET_VALIDATION_TTL = LICENSE_CACHE_DURATION;
|
|
84
|
-
var LICENSE_HOLD_STORAGE_KEY = "buoy-gg-license-hold";
|
|
85
|
-
|
|
86
|
-
// src/api.ts
|
|
87
|
-
var { apiUrl } = KEYGEN_CONFIG;
|
|
88
|
-
var LicenseTransportError = class extends Error {
|
|
89
|
-
constructor(message, status, retryAfterMs = null) {
|
|
90
|
-
super(message);
|
|
91
|
-
/** Discriminator that survives bundling, subclassing and realm boundaries. */
|
|
92
|
-
this.isTransient = true;
|
|
93
|
-
this.name = "LicenseTransportError";
|
|
94
|
-
this.status = status;
|
|
95
|
-
this.retryAfterMs = retryAfterMs;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
function isRateLimited(error) {
|
|
99
|
-
const status = error?.status;
|
|
100
|
-
return status === 429 || status === 402;
|
|
101
|
-
}
|
|
102
|
-
function backoffAfter(error, quotaCooldown, defaultWait) {
|
|
103
|
-
if (error.status === 402) return quotaCooldown;
|
|
104
|
-
return error.retryAfterMs ?? defaultWait;
|
|
105
|
-
}
|
|
106
|
-
function readRetryAfter(response) {
|
|
107
|
-
const get = (name) => {
|
|
108
|
-
try {
|
|
109
|
-
return response.headers?.get?.(name) ?? null;
|
|
110
|
-
} catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
const retryAfter = get("Retry-After");
|
|
115
|
-
if (retryAfter) {
|
|
116
|
-
const seconds = Number(retryAfter);
|
|
117
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
118
|
-
const date = Date.parse(retryAfter);
|
|
119
|
-
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
|
|
120
|
-
}
|
|
121
|
-
const reset = Number(get("X-RateLimit-Reset"));
|
|
122
|
-
if (Number.isFinite(reset) && reset > 0) return Math.max(0, reset * 1e3 - Date.now());
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
function isTransientLicenseError(error) {
|
|
126
|
-
if (error instanceof LicenseTransportError) return true;
|
|
127
|
-
if (error instanceof TypeError) return true;
|
|
128
|
-
return typeof error === "object" && error !== null && error.isTransient === true;
|
|
129
|
-
}
|
|
130
|
-
async function describeFailure(response) {
|
|
131
|
-
try {
|
|
132
|
-
const errorData = await response.json();
|
|
133
|
-
return errorData.errors?.[0]?.detail || `HTTP ${response.status}`;
|
|
134
|
-
} catch {
|
|
135
|
-
return `HTTP ${response.status}`;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function getBaseHeaders() {
|
|
139
|
-
return {
|
|
140
|
-
"Content-Type": "application/vnd.api+json",
|
|
141
|
-
Accept: "application/vnd.api+json"
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
async function requestValidation(licenseKey, signal) {
|
|
145
|
-
const url = `${apiUrl}/validate`;
|
|
146
|
-
const body = {
|
|
147
|
-
meta: {
|
|
148
|
-
key: licenseKey
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
let response;
|
|
152
|
-
try {
|
|
153
|
-
response = await fetch(url, {
|
|
154
|
-
signal,
|
|
155
|
-
method: "POST",
|
|
156
|
-
headers: getBaseHeaders(),
|
|
157
|
-
body: JSON.stringify(body)
|
|
158
|
-
});
|
|
159
|
-
} catch (error) {
|
|
160
|
-
throw new LicenseTransportError(
|
|
161
|
-
error instanceof Error ? error.message : "Network request failed",
|
|
162
|
-
null
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
if (!response.ok) {
|
|
166
|
-
throw new LicenseTransportError(
|
|
167
|
-
await describeFailure(response),
|
|
168
|
-
response.status,
|
|
169
|
-
response.status === 429 ? readRetryAfter(response) : null
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
try {
|
|
173
|
-
const json = await response.json();
|
|
174
|
-
if (!json || typeof json !== "object" || typeof json.meta?.valid !== "boolean" || typeof json.meta?.code !== "string" || !json.meta.code) throw new Error("Missing license verdict");
|
|
175
|
-
return json;
|
|
176
|
-
} catch {
|
|
177
|
-
throw new LicenseTransportError(
|
|
178
|
-
"License server returned an unreadable response",
|
|
179
|
-
response.status
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
async function validateLicense(key) {
|
|
184
|
-
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
185
|
-
let timer;
|
|
186
|
-
try {
|
|
187
|
-
return await Promise.race([
|
|
188
|
-
requestValidation(key, controller?.signal),
|
|
189
|
-
new Promise((_, reject) => {
|
|
190
|
-
timer = setTimeout(() => {
|
|
191
|
-
reject(new LicenseTransportError("License verification timed out. Please retry.", null));
|
|
192
|
-
controller?.abort();
|
|
193
|
-
}, 15e3);
|
|
194
|
-
})
|
|
195
|
-
]);
|
|
196
|
-
} finally {
|
|
197
|
-
if (timer) clearTimeout(timer);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function isAcceptedValidation(response) {
|
|
201
|
-
if (!response.data) return false;
|
|
202
|
-
const product = response.data.relationships?.product?.data?.id;
|
|
203
|
-
if (product && product !== KEYGEN_CONFIG.productId) return false;
|
|
204
|
-
if (response.meta.valid && response.meta.code === "VALID") return true;
|
|
205
|
-
return [
|
|
206
|
-
"OVERDUE",
|
|
207
|
-
"NO_MACHINE",
|
|
208
|
-
"NO_MACHINES",
|
|
209
|
-
"TOO_MANY_MACHINES",
|
|
210
|
-
"TOO_MANY_CORES",
|
|
211
|
-
"TOO_MANY_PROCESSES",
|
|
212
|
-
"FINGERPRINT_SCOPE_MISMATCH",
|
|
213
|
-
"HEARTBEAT_NOT_STARTED",
|
|
214
|
-
"HEARTBEAT_DEAD"
|
|
215
|
-
].includes(response.meta.code);
|
|
216
|
-
}
|
|
217
|
-
function machineCount(response) {
|
|
218
|
-
const count = response.data?.relationships?.machines?.meta?.count;
|
|
219
|
-
return typeof count === "number" && Number.isFinite(count) ? count : 0;
|
|
220
|
-
}
|
|
221
|
-
async function activateMachine(licenseId, licenseKey, fingerprint, name, metadata) {
|
|
222
|
-
const url = `${apiUrl}/machines`;
|
|
223
|
-
const response = await fetch(url, {
|
|
224
|
-
method: "POST",
|
|
225
|
-
headers: getBaseHeaders(),
|
|
226
|
-
body: JSON.stringify({ licenseId, licenseKey, fingerprint, name, metadata: metadata || {} })
|
|
227
|
-
});
|
|
228
|
-
if (!response.ok) {
|
|
229
|
-
const errorData = await response.json();
|
|
230
|
-
const errorCode = errorData.errors?.[0]?.code;
|
|
231
|
-
if (errorCode === "FINGERPRINT_TAKEN") {
|
|
232
|
-
throw new Error("FINGERPRINT_TAKEN");
|
|
233
|
-
}
|
|
234
|
-
if (errorCode === "MACHINE_LIMIT_EXCEEDED") {
|
|
235
|
-
throw new Error("MACHINE_LIMIT_EXCEEDED");
|
|
236
|
-
}
|
|
237
|
-
const errorMessage = errorData.errors?.[0]?.detail || `HTTP ${response.status}`;
|
|
238
|
-
throw new Error(errorMessage);
|
|
239
|
-
}
|
|
240
|
-
return response.json();
|
|
241
|
-
}
|
|
242
|
-
function parseValidationResult(response) {
|
|
243
|
-
const { meta, data } = response;
|
|
244
|
-
return {
|
|
245
|
-
valid: meta.valid,
|
|
246
|
-
code: meta.code,
|
|
247
|
-
detail: meta.detail,
|
|
248
|
-
// data can be null for invalid licenses
|
|
249
|
-
metadata: data?.attributes?.metadata
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
function isFatalLicenseError(code) {
|
|
253
|
-
return [
|
|
254
|
-
"NOT_FOUND",
|
|
255
|
-
"SUSPENDED",
|
|
256
|
-
"EXPIRED",
|
|
257
|
-
"BANNED",
|
|
258
|
-
"PRODUCT_SCOPE_MISMATCH"
|
|
259
|
-
].includes(code);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/device-info.web.ts
|
|
263
|
-
function generateUUID() {
|
|
264
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
265
|
-
return crypto.randomUUID();
|
|
266
|
-
}
|
|
267
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
268
|
-
const r = Math.random() * 16 | 0;
|
|
269
|
-
const v = c === "x" ? r : r & 3 | 8;
|
|
270
|
-
return v.toString(16);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
function isDevBundle() {
|
|
274
|
-
if (typeof __DEV__ !== "undefined") return __DEV__;
|
|
275
|
-
try {
|
|
276
|
-
if (typeof navigator !== "undefined" && /electron/i.test(navigator.userAgent)) return true;
|
|
277
|
-
} catch {
|
|
278
|
-
}
|
|
279
|
-
return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
|
|
280
|
-
}
|
|
281
|
-
function detectPlatform() {
|
|
282
|
-
if (typeof navigator === "undefined") {
|
|
283
|
-
return "unknown";
|
|
284
|
-
}
|
|
285
|
-
const ua = navigator.userAgent.toLowerCase();
|
|
286
|
-
const platform = navigator.platform?.toLowerCase() || "";
|
|
287
|
-
if (ua.includes("electron")) {
|
|
288
|
-
if (platform.includes("mac") || ua.includes("mac")) {
|
|
289
|
-
return "electron-macos";
|
|
290
|
-
}
|
|
291
|
-
if (platform.includes("win") || ua.includes("win")) {
|
|
292
|
-
return "electron-windows";
|
|
293
|
-
}
|
|
294
|
-
if (platform.includes("linux") || ua.includes("linux")) {
|
|
295
|
-
return "electron-linux";
|
|
296
|
-
}
|
|
297
|
-
return "electron";
|
|
298
|
-
}
|
|
299
|
-
if (platform.includes("mac") || ua.includes("mac")) {
|
|
300
|
-
return "macos";
|
|
301
|
-
}
|
|
302
|
-
if (platform.includes("win") || ua.includes("win")) {
|
|
303
|
-
return "windows";
|
|
304
|
-
}
|
|
305
|
-
if (platform.includes("linux") || ua.includes("linux")) {
|
|
306
|
-
return "linux";
|
|
307
|
-
}
|
|
308
|
-
if (/iphone|ipad|ipod/.test(ua)) {
|
|
309
|
-
return "ios-web";
|
|
310
|
-
}
|
|
311
|
-
if (ua.includes("android")) {
|
|
312
|
-
return "android-web";
|
|
313
|
-
}
|
|
314
|
-
return "web";
|
|
315
|
-
}
|
|
316
|
-
function getOSVersion() {
|
|
317
|
-
if (typeof navigator === "undefined") {
|
|
318
|
-
return "unknown";
|
|
319
|
-
}
|
|
320
|
-
const ua = navigator.userAgent;
|
|
321
|
-
const macMatch = ua.match(/Mac OS X ([\d_.]+)/);
|
|
322
|
-
if (macMatch) {
|
|
323
|
-
return macMatch[1].replace(/_/g, ".");
|
|
324
|
-
}
|
|
325
|
-
const winMatch = ua.match(/Windows NT ([\d.]+)/);
|
|
326
|
-
if (winMatch) {
|
|
327
|
-
const versions = {
|
|
328
|
-
"10.0": "10/11",
|
|
329
|
-
"6.3": "8.1",
|
|
330
|
-
"6.2": "8",
|
|
331
|
-
"6.1": "7"
|
|
332
|
-
};
|
|
333
|
-
return versions[winMatch[1]] || winMatch[1];
|
|
334
|
-
}
|
|
335
|
-
if (ua.includes("Linux")) {
|
|
336
|
-
return "Linux";
|
|
337
|
-
}
|
|
338
|
-
return "unknown";
|
|
339
|
-
}
|
|
340
|
-
function getBrowserInfo() {
|
|
341
|
-
if (typeof navigator === "undefined") {
|
|
342
|
-
return { name: "unknown", version: "unknown" };
|
|
343
|
-
}
|
|
344
|
-
const ua = navigator.userAgent;
|
|
345
|
-
const electronMatch = ua.match(/Electron\/([\d.]+)/);
|
|
346
|
-
if (electronMatch) {
|
|
347
|
-
return { name: "Electron", version: electronMatch[1] };
|
|
348
|
-
}
|
|
349
|
-
const chromeMatch = ua.match(/Chrome\/([\d.]+)/);
|
|
350
|
-
if (chromeMatch && !ua.includes("Edg")) {
|
|
351
|
-
return { name: "Chrome", version: chromeMatch[1] };
|
|
352
|
-
}
|
|
353
|
-
const edgeMatch = ua.match(/Edg\/([\d.]+)/);
|
|
354
|
-
if (edgeMatch) {
|
|
355
|
-
return { name: "Edge", version: edgeMatch[1] };
|
|
356
|
-
}
|
|
357
|
-
const firefoxMatch = ua.match(/Firefox\/([\d.]+)/);
|
|
358
|
-
if (firefoxMatch) {
|
|
359
|
-
return { name: "Firefox", version: firefoxMatch[1] };
|
|
360
|
-
}
|
|
361
|
-
const safariMatch = ua.match(/Version\/([\d.]+).*Safari/);
|
|
362
|
-
if (safariMatch) {
|
|
363
|
-
return { name: "Safari", version: safariMatch[1] };
|
|
364
|
-
}
|
|
365
|
-
return { name: "unknown", version: "unknown" };
|
|
366
|
-
}
|
|
367
|
-
function getDeviceMetadata() {
|
|
368
|
-
const metadata = {
|
|
369
|
-
platform: detectPlatform(),
|
|
370
|
-
osVersion: getOSVersion(),
|
|
371
|
-
isSimulator: "false"
|
|
372
|
-
};
|
|
373
|
-
const browserInfo = getBrowserInfo();
|
|
374
|
-
metadata.browser = browserInfo.name;
|
|
375
|
-
metadata.browserVersion = browserInfo.version;
|
|
376
|
-
if (typeof window !== "undefined" && window.screen) {
|
|
377
|
-
metadata.screenResolution = `${window.screen.width}x${window.screen.height}`;
|
|
378
|
-
}
|
|
379
|
-
if (typeof navigator !== "undefined" && navigator.language) {
|
|
380
|
-
metadata.language = navigator.language;
|
|
381
|
-
}
|
|
382
|
-
try {
|
|
383
|
-
metadata.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
384
|
-
} catch {
|
|
385
|
-
}
|
|
386
|
-
return metadata;
|
|
387
|
-
}
|
|
388
|
-
function getDefaultDeviceName() {
|
|
389
|
-
const platform = detectPlatform();
|
|
390
|
-
const browserInfo = getBrowserInfo();
|
|
391
|
-
if (platform.startsWith("electron")) {
|
|
392
|
-
const os = platform.replace("electron-", "").replace("electron", "Desktop");
|
|
393
|
-
return `${os.charAt(0).toUpperCase() + os.slice(1)} App`;
|
|
394
|
-
}
|
|
395
|
-
if (browserInfo.name !== "unknown") {
|
|
396
|
-
return `${browserInfo.name} Browser`;
|
|
397
|
-
}
|
|
398
|
-
return "Web Browser";
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// src/cacheSignature.ts
|
|
402
|
-
var SIGNATURE_VERSION = "v2";
|
|
403
|
-
var LEGACY_VERSIONS = ["v1"];
|
|
404
|
-
var SIGNING_SALT = "buoy.gg/pro:9f3c1d7a4b2e:license-cache";
|
|
405
|
-
var FIELD_SEPARATOR = "\0";
|
|
406
|
-
function fnv1a(input, seed) {
|
|
407
|
-
let hash = seed >>> 0;
|
|
408
|
-
for (let i = 0; i < input.length; i++) {
|
|
409
|
-
hash ^= input.charCodeAt(i);
|
|
410
|
-
hash = Math.imul(hash, 16777619) >>> 0;
|
|
411
|
-
}
|
|
412
|
-
return hash >>> 0;
|
|
413
|
-
}
|
|
414
|
-
function canonicalize(cache, version) {
|
|
415
|
-
const fields = [
|
|
416
|
-
version,
|
|
417
|
-
cache.licenseKey,
|
|
418
|
-
cache.licenseId,
|
|
419
|
-
cache.isPro ? "1" : "0",
|
|
420
|
-
cache.expiresAt ?? "",
|
|
421
|
-
String(cache.cachedAt)
|
|
422
|
-
];
|
|
423
|
-
if (version !== "v1") {
|
|
424
|
-
fields.push(cache.tier ?? "");
|
|
425
|
-
}
|
|
426
|
-
fields.push(SIGNING_SALT);
|
|
427
|
-
return fields.join(FIELD_SEPARATOR);
|
|
428
|
-
}
|
|
429
|
-
function signatureFor(cache, version) {
|
|
430
|
-
const input = canonicalize(cache, version);
|
|
431
|
-
const a = fnv1a(input, 2166136261);
|
|
432
|
-
const b = fnv1a(input, 2135587861);
|
|
433
|
-
return `${version}.${a.toString(16)}${b.toString(16)}`;
|
|
434
|
-
}
|
|
435
|
-
function cacheSignature(cache) {
|
|
436
|
-
return signatureFor(cache, SIGNATURE_VERSION);
|
|
437
|
-
}
|
|
438
|
-
function signCache(cache) {
|
|
439
|
-
return { ...cache, sig: cacheSignature(cache) };
|
|
440
|
-
}
|
|
441
|
-
function verifyCacheSignature(cache) {
|
|
442
|
-
if (!cache || typeof cache.sig !== "string" || cache.sig.length === 0) {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
if (cache.sig === cacheSignature(cache)) return true;
|
|
446
|
-
return LEGACY_VERSIONS.some(
|
|
447
|
-
(version) => cache.sig.startsWith(`${version}.`) && cache.sig === signatureFor(cache, version)
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// src/freeToken.ts
|
|
452
|
-
import nacl from "tweetnacl";
|
|
453
|
-
var FREE_TOKEN_VERSION = 1;
|
|
454
|
-
var FREE_TOKEN_TIER_FREE = 1;
|
|
455
|
-
var FREE_TOKEN_DOMAIN = "buoy-free-token-v1:";
|
|
456
|
-
var PAYLOAD_LENGTH = 22;
|
|
457
|
-
var SIGNATURE_LENGTH = 64;
|
|
458
|
-
var TOKEN_PATTERN = /^BUOY1-([A-Z2-7]+)-([A-Z2-7]+)$/;
|
|
459
|
-
function isBuoyToken(key) {
|
|
460
|
-
return TOKEN_PATTERN.test(key.trim().toUpperCase());
|
|
461
|
-
}
|
|
462
|
-
var B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
463
|
-
function base32Decode(text) {
|
|
464
|
-
const out = [];
|
|
465
|
-
let bits = 0;
|
|
466
|
-
let value = 0;
|
|
467
|
-
for (const char of text) {
|
|
468
|
-
const index = B32.indexOf(char);
|
|
469
|
-
if (index === -1) return null;
|
|
470
|
-
value = value << 5 | index;
|
|
471
|
-
bits += 5;
|
|
472
|
-
if (bits >= 8) {
|
|
473
|
-
out.push(value >>> bits - 8 & 255);
|
|
474
|
-
bits -= 8;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return new Uint8Array(out);
|
|
478
|
-
}
|
|
479
|
-
function hexToBytes(hex) {
|
|
480
|
-
const clean = hex.trim().toLowerCase();
|
|
481
|
-
const out = new Uint8Array(clean.length / 2);
|
|
482
|
-
for (let i = 0; i < out.length; i += 1) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
483
|
-
return out;
|
|
484
|
-
}
|
|
485
|
-
function bytesToHex(bytes) {
|
|
486
|
-
let out = "";
|
|
487
|
-
for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
|
|
488
|
-
return out;
|
|
489
|
-
}
|
|
490
|
-
function utf8(text) {
|
|
491
|
-
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(text);
|
|
492
|
-
const out = new Uint8Array(text.length);
|
|
493
|
-
for (let i = 0; i < text.length; i += 1) out[i] = text.charCodeAt(i) & 255;
|
|
494
|
-
return out;
|
|
495
|
-
}
|
|
496
|
-
function concat(a, b) {
|
|
497
|
-
const out = new Uint8Array(a.length + b.length);
|
|
498
|
-
out.set(a, 0);
|
|
499
|
-
out.set(b, a.length);
|
|
500
|
-
return out;
|
|
501
|
-
}
|
|
502
|
-
function signedMessage(payload) {
|
|
503
|
-
return concat(utf8(FREE_TOKEN_DOMAIN), payload);
|
|
504
|
-
}
|
|
505
|
-
var activePublicKey = FREE_TOKEN_PUBLIC_KEY;
|
|
506
|
-
function verifyFreeToken(key, publicKeyHex = activePublicKey) {
|
|
507
|
-
const match = TOKEN_PATTERN.exec(key.trim().toUpperCase());
|
|
508
|
-
if (!match) return null;
|
|
509
|
-
const payload = base32Decode(match[1]);
|
|
510
|
-
const signature = base32Decode(match[2]);
|
|
511
|
-
if (!payload || !signature) return null;
|
|
512
|
-
if (payload.length !== PAYLOAD_LENGTH || signature.length !== SIGNATURE_LENGTH) return null;
|
|
513
|
-
if (payload[0] !== FREE_TOKEN_VERSION || payload[1] !== FREE_TOKEN_TIER_FREE) return null;
|
|
514
|
-
try {
|
|
515
|
-
if (!nacl.sign.detached.verify(signedMessage(payload), signature, hexToBytes(publicKeyHex))) return null;
|
|
516
|
-
} catch {
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
const seconds = (payload[2] << 24 >>> 0) + (payload[3] << 16) + (payload[4] << 8) + payload[5];
|
|
520
|
-
return { tier: "free", issuedAt: seconds * 1e3, accountHash: bytesToHex(payload.slice(6, 22)) };
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// src/tier.ts
|
|
524
|
-
function resolveTier(license) {
|
|
525
|
-
const policyId = license.relationships?.policy?.data?.id;
|
|
526
|
-
if (policyId === KEYGEN_POLICIES.free) return "free";
|
|
527
|
-
if (policyId === KEYGEN_POLICIES.solo || policyId === KEYGEN_POLICIES.business) {
|
|
528
|
-
return "pro";
|
|
529
|
-
}
|
|
530
|
-
const plan = license.attributes?.metadata?.plan;
|
|
531
|
-
if (plan === "free") return "free";
|
|
532
|
-
return "pro";
|
|
533
|
-
}
|
|
534
|
-
function isRecognizedAccount(license) {
|
|
535
|
-
if (!license) return false;
|
|
536
|
-
const product = license.relationships?.product?.data?.id;
|
|
537
|
-
if (product) return product === KEYGEN_CONFIG.productId;
|
|
538
|
-
const policy = license.relationships?.policy?.data?.id;
|
|
539
|
-
return Object.values(KEYGEN_POLICIES).some((id) => id === policy);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// src/accountAccess.ts
|
|
543
|
-
function resolveAccountAccess(input, now = Date.now()) {
|
|
544
|
-
const validUntil = input.verifiedAt !== null && Number.isFinite(input.verifiedAt) && input.verifiedAt <= now ? input.verifiedAt + LICENSE_CACHE_DURATION : null;
|
|
545
|
-
if (input.initialized && input.key && input.tierSource !== "none" && validUntil !== null && now < validUntil) {
|
|
546
|
-
return { status: "admitted", reason: null, validUntil };
|
|
547
|
-
}
|
|
548
|
-
if (validUntil !== null && now >= validUntil) {
|
|
549
|
-
return { status: "locked", reason: "verification_expired", validUntil };
|
|
550
|
-
}
|
|
551
|
-
if (!input.initialized || input.validating) {
|
|
552
|
-
return { status: "initializing", reason: "checking", validUntil };
|
|
553
|
-
}
|
|
554
|
-
return {
|
|
555
|
-
status: "locked",
|
|
556
|
-
reason: validUntil !== null && now >= validUntil ? "verification_expired" : "account_required",
|
|
557
|
-
validUntil
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// src/foreground.web.ts
|
|
562
|
-
function subscribeForeground(refresh) {
|
|
563
|
-
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return () => {
|
|
564
|
-
};
|
|
565
|
-
const visible = () => {
|
|
566
|
-
if (!document.hidden) refresh();
|
|
567
|
-
};
|
|
568
|
-
window.addEventListener("focus", refresh);
|
|
569
|
-
document.addEventListener("visibilitychange", visible);
|
|
570
|
-
return () => {
|
|
571
|
-
window.removeEventListener("focus", refresh);
|
|
572
|
-
document.removeEventListener("visibilitychange", visible);
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// src/LicenseManager.ts
|
|
577
|
-
async function boundedStorage(operation, fallback) {
|
|
578
|
-
let timer;
|
|
579
|
-
try {
|
|
580
|
-
return await Promise.race([operation, new Promise((resolve) => {
|
|
581
|
-
timer = setTimeout(() => resolve(fallback), 15e3);
|
|
582
|
-
})]);
|
|
583
|
-
} finally {
|
|
584
|
-
if (timer) clearTimeout(timer);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
function tierOfCache(cached) {
|
|
588
|
-
if (cached.tier === "pro" || cached.tier === "free") return cached.tier;
|
|
589
|
-
return cached.isPro ? "pro" : "free";
|
|
590
|
-
}
|
|
591
|
-
var LicenseManagerImpl = class {
|
|
592
|
-
constructor() {
|
|
593
|
-
this.state = {
|
|
594
|
-
isInitialized: false,
|
|
595
|
-
isValidating: false,
|
|
596
|
-
isPro: false,
|
|
597
|
-
tier: "free",
|
|
598
|
-
tierSource: "none",
|
|
599
|
-
licenseKey: null,
|
|
600
|
-
error: null,
|
|
601
|
-
errorKind: null,
|
|
602
|
-
expiresAt: null
|
|
603
|
-
};
|
|
604
|
-
this.listeners = /* @__PURE__ */ new Set();
|
|
605
|
-
this.revalidationTimer = null;
|
|
606
|
-
this.cachedLicense = null;
|
|
607
|
-
this.fingerprintPromise = null;
|
|
608
|
-
// Promise for initialization to prevent double-init race condition
|
|
609
|
-
this.initPromise = null;
|
|
610
|
-
/**
|
|
611
|
-
* Bumped whenever the license is deliberately changed or cleared.
|
|
612
|
-
*
|
|
613
|
-
* `initialize()` kicks off a background revalidation of the cached key and
|
|
614
|
-
* does NOT await it, so a `clearLicense()` that lands in between would be
|
|
615
|
-
* silently undone: the in-flight response re-applies the old tier and
|
|
616
|
-
* re-persists the cache blob that was just deleted. Removing a key would
|
|
617
|
-
* then appear to do nothing — Pro comes straight back a moment later.
|
|
618
|
-
*
|
|
619
|
-
* Background work captures this value when it starts and drops its result if
|
|
620
|
-
* it no longer matches.
|
|
621
|
-
*/
|
|
622
|
-
this.generation = 0;
|
|
623
|
-
this.persistence = Promise.resolve();
|
|
624
|
-
this.verifiedAt = null;
|
|
625
|
-
this.expiryTimer = null;
|
|
626
|
-
this.removeForegroundListener = null;
|
|
627
|
-
/**
|
|
628
|
-
* Pending backoff retry after a transient failure (see
|
|
629
|
-
* LICENSE_TRANSIENT_RETRY_DELAYS). At most one is ever outstanding, and it
|
|
630
|
-
* is cancelled by any deliberate credential change or by a success.
|
|
631
|
-
*/
|
|
632
|
-
this.retryTimer = null;
|
|
633
|
-
this.retryAttempt = 0;
|
|
634
|
-
// Device registration callback for UI integration
|
|
635
|
-
this.deviceRegistrationCallback = null;
|
|
636
|
-
this.pendingRegistration = null;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Initialize the license manager and load cached license
|
|
640
|
-
*/
|
|
641
|
-
async initialize() {
|
|
642
|
-
if (this.initPromise) return this.initPromise;
|
|
643
|
-
if (this.state.isInitialized) return;
|
|
644
|
-
this.initPromise = this._doInitialize();
|
|
645
|
-
return this.initPromise;
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Internal initialization logic
|
|
649
|
-
*/
|
|
650
|
-
async _doInitialize() {
|
|
651
|
-
if (this.state.isInitialized) return;
|
|
652
|
-
const startedAt = this.generation;
|
|
653
|
-
try {
|
|
654
|
-
const cached = await boundedStorage(this.loadCachedLicense(), null);
|
|
655
|
-
if (startedAt !== this.generation) {
|
|
656
|
-
this.state.isInitialized = true;
|
|
657
|
-
this.emit({ type: "initialized", isPro: this.state.isPro });
|
|
658
|
-
this.startRevalidationTimer();
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if (cached && (this.configuredKey === void 0 || cached.licenseKey === this.configuredKey)) {
|
|
662
|
-
const trusted = verifyCacheSignature(cached);
|
|
663
|
-
this.cachedLicense = cached;
|
|
664
|
-
this.state.licenseKey = cached.licenseKey;
|
|
665
|
-
if (trusted && this.isCacheValid(cached)) this.applyTier(tierOfCache(cached));
|
|
666
|
-
else this.lock();
|
|
667
|
-
this.state.expiresAt = cached.expiresAt ? new Date(cached.expiresAt) : null;
|
|
668
|
-
if (trusted && this.isCacheValid(cached)) this.setVerifiedAt(cached.cachedAt);
|
|
669
|
-
if (this.configuredKey === void 0 && cached.licenseKey && !(trusted && this.isCacheValid(cached) && this.isCacheFresh(cached))) {
|
|
670
|
-
this.validateInBackground(cached.licenseKey);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
this.state.isInitialized = true;
|
|
674
|
-
this.emit({ type: "initialized", isPro: this.state.isPro });
|
|
675
|
-
this.startRevalidationTimer();
|
|
676
|
-
} catch (error) {
|
|
677
|
-
console.warn("[React Buoy] License initialization failed:", error);
|
|
678
|
-
this.state.isInitialized = true;
|
|
679
|
-
this.emit({ type: "initialized", isPro: false });
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
/** Omitted configuration preserves activation; an explicit blank signs out. */
|
|
683
|
-
async configure(key) {
|
|
684
|
-
if (key === void 0) {
|
|
685
|
-
await this.initialize();
|
|
686
|
-
return this.hasAccountAccess();
|
|
687
|
-
}
|
|
688
|
-
const normalized = key.trim().toUpperCase();
|
|
689
|
-
const startedAt = ++this.generation;
|
|
690
|
-
this.cancelRetry();
|
|
691
|
-
this.configuredKey = normalized || null;
|
|
692
|
-
if (this.state.licenseKey !== this.configuredKey) this.lock();
|
|
693
|
-
this.state.licenseKey = this.configuredKey;
|
|
694
|
-
this.state.isValidating = !!normalized;
|
|
695
|
-
this.emit({ type: "validating", isPro: this.isPro() });
|
|
696
|
-
await this.initialize();
|
|
697
|
-
if (startedAt !== this.generation) return false;
|
|
698
|
-
if (!normalized) {
|
|
699
|
-
await this.clearLicense();
|
|
700
|
-
return false;
|
|
701
|
-
}
|
|
702
|
-
return this.setLicenseKey(normalized);
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Set and validate a license key.
|
|
706
|
-
*
|
|
707
|
-
* Simple validation - just checks if the key is valid.
|
|
708
|
-
* No device registration or limits - we trust our customers.
|
|
709
|
-
* Keygen tracks validation calls for usage analytics.
|
|
710
|
-
*/
|
|
711
|
-
async setLicenseKey(licenseKey, opts) {
|
|
712
|
-
if (!licenseKey || licenseKey.trim().length === 0) {
|
|
713
|
-
this.state.error = "License key is required";
|
|
714
|
-
this.state.errorKind = "verdict";
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
const normalizedKey = licenseKey.trim().toUpperCase();
|
|
718
|
-
const startedAt = ++this.generation;
|
|
719
|
-
this.cancelRetry();
|
|
720
|
-
if (!opts?.isRetry) this.retryAttempt = 0;
|
|
721
|
-
if (this.state.licenseKey !== normalizedKey) this.lock();
|
|
722
|
-
this.state.licenseKey = normalizedKey;
|
|
723
|
-
this.state.isValidating = true;
|
|
724
|
-
this.state.error = null;
|
|
725
|
-
this.state.errorKind = null;
|
|
726
|
-
this.emit({ type: "validating", isPro: this.isPro() });
|
|
727
|
-
if (isBuoyToken(normalizedKey)) return this.admitToken(normalizedKey, startedAt);
|
|
728
|
-
if (!this.cachedLicense) {
|
|
729
|
-
const cached = await boundedStorage(this.loadCachedLicense(), null);
|
|
730
|
-
if (startedAt !== this.generation) return false;
|
|
731
|
-
if (cached?.licenseKey === normalizedKey) {
|
|
732
|
-
this.cachedLicense = cached;
|
|
733
|
-
if (verifyCacheSignature(cached) && this.isCacheValid(cached)) {
|
|
734
|
-
this.applyTier(tierOfCache(cached));
|
|
735
|
-
this.state.expiresAt = cached.expiresAt ? new Date(cached.expiresAt) : null;
|
|
736
|
-
this.setVerifiedAt(cached.cachedAt);
|
|
737
|
-
this.emit({ type: "validating", isPro: this.isPro() });
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
const fresh = this.cachedLicense;
|
|
742
|
-
if (!opts?.force && fresh && fresh.valid && fresh.licenseKey === normalizedKey && fresh.fingerprint && verifyCacheSignature(fresh) && this.isCacheValid(fresh) && this.isCacheFresh(fresh)) {
|
|
743
|
-
this.applyTier(tierOfCache(fresh));
|
|
744
|
-
this.state.licenseKey = normalizedKey;
|
|
745
|
-
this.state.isValidating = false;
|
|
746
|
-
this.state.error = null;
|
|
747
|
-
this.state.errorKind = null;
|
|
748
|
-
this.state.expiresAt = fresh.expiresAt ? new Date(fresh.expiresAt) : null;
|
|
749
|
-
this.setVerifiedAt(fresh.cachedAt);
|
|
750
|
-
this.emit({ type: "validated", isPro: this.state.isPro });
|
|
751
|
-
return true;
|
|
752
|
-
}
|
|
753
|
-
const hold = await boundedStorage(this.loadHold(), null);
|
|
754
|
-
if (startedAt !== this.generation) return false;
|
|
755
|
-
const remainingBackoff = (hold?.backoffUntil ?? 0) - Date.now();
|
|
756
|
-
if (remainingBackoff > 0) {
|
|
757
|
-
return this.standDown(normalizedKey, startedAt, "License server is rate limited; retrying later.", remainingBackoff);
|
|
758
|
-
}
|
|
759
|
-
if (!opts?.force && hold?.key === normalizedKey && hold.verdictAt !== void 0 && Date.now() - hold.verdictAt >= 0 && Date.now() - hold.verdictAt < LICENSE_VERDICT_CACHE_DURATION) {
|
|
760
|
-
this.lock();
|
|
761
|
-
this.state.isValidating = false;
|
|
762
|
-
this.state.error = hold.detail || "License could not be verified";
|
|
763
|
-
this.state.errorKind = "verdict";
|
|
764
|
-
this.emit({ type: "error", isPro: false, error: this.state.error });
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
let response;
|
|
768
|
-
try {
|
|
769
|
-
response = await validateLicense(normalizedKey);
|
|
770
|
-
} catch (error) {
|
|
771
|
-
const errorMessage = error instanceof Error ? error.message : "Validation failed";
|
|
772
|
-
if (startedAt !== this.generation) return false;
|
|
773
|
-
if (isTransientLicenseError(error)) {
|
|
774
|
-
if (isRateLimited(error)) {
|
|
775
|
-
const wait = Math.min(
|
|
776
|
-
backoffAfter(error, LICENSE_QUOTA_COOLDOWN, LICENSE_RATE_LIMIT_DEFAULT_WAIT) * 2 ** this.retryAttempt,
|
|
777
|
-
LICENSE_QUOTA_COOLDOWN
|
|
778
|
-
);
|
|
779
|
-
this.retryAttempt += 1;
|
|
780
|
-
await this.saveHold({ ...this.hold, backoffUntil: Date.now() + wait });
|
|
781
|
-
if (startedAt !== this.generation) return false;
|
|
782
|
-
return this.standDown(normalizedKey, startedAt, errorMessage, wait);
|
|
783
|
-
}
|
|
784
|
-
this.state.isValidating = false;
|
|
785
|
-
if (this.cachedLicense && // The cache can only stand in for the key it was written FOR. Without
|
|
786
|
-
// this, typing a new (or mistyped) key while the server is
|
|
787
|
-
// unreachable would "validate" it by serving the old key's cache.
|
|
788
|
-
this.cachedLicense.licenseKey === normalizedKey && verifyCacheSignature(this.cachedLicense) && this.isCacheValid(this.cachedLicense)) {
|
|
789
|
-
this.applyTier(tierOfCache(this.cachedLicense));
|
|
790
|
-
this.state.error = null;
|
|
791
|
-
this.state.errorKind = null;
|
|
792
|
-
this.state.expiresAt = this.cachedLicense.expiresAt ? new Date(this.cachedLicense.expiresAt) : null;
|
|
793
|
-
this.setVerifiedAt(this.cachedLicense.cachedAt);
|
|
794
|
-
if (!this.isPro()) this.scheduleRetry(normalizedKey);
|
|
795
|
-
this.emit({ type: "validated", isPro: this.state.isPro });
|
|
796
|
-
return true;
|
|
797
|
-
}
|
|
798
|
-
this.state.error = errorMessage;
|
|
799
|
-
this.state.errorKind = "transport";
|
|
800
|
-
this.scheduleRetry(normalizedKey);
|
|
801
|
-
this.emit({
|
|
802
|
-
type: "error",
|
|
803
|
-
isPro: this.state.isPro,
|
|
804
|
-
error: errorMessage
|
|
805
|
-
});
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
|
-
this.state.isValidating = false;
|
|
809
|
-
this.state.error = errorMessage;
|
|
810
|
-
this.state.errorKind = "verdict";
|
|
811
|
-
this.emit({ type: "error", isPro: false, error: errorMessage });
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
try {
|
|
815
|
-
if (startedAt !== this.generation) return false;
|
|
816
|
-
const result = parseValidationResult(response);
|
|
817
|
-
const product = response.data?.relationships?.product?.data?.id;
|
|
818
|
-
if ((!product || product === KEYGEN_CONFIG.productId) && result.code === "EXPIRED" && (isRecognizedAccount(response.data) || this.cachedLicense?.licenseKey === normalizedKey && verifyCacheSignature(this.cachedLicense) && this.isCacheValid(this.cachedLicense))) {
|
|
819
|
-
const now2 = Date.now();
|
|
820
|
-
const saved2 = await this.saveCache({
|
|
821
|
-
licenseKey: normalizedKey,
|
|
822
|
-
licenseId: response.data?.id ?? "",
|
|
823
|
-
valid: true,
|
|
824
|
-
isPro: false,
|
|
825
|
-
tier: "free",
|
|
826
|
-
expiresAt: null,
|
|
827
|
-
cachedAt: now2,
|
|
828
|
-
lastValidatedAt: now2,
|
|
829
|
-
fingerprint: this.cachedLicense?.licenseKey === normalizedKey ? this.cachedLicense.fingerprint : void 0
|
|
830
|
-
}, startedAt);
|
|
831
|
-
if (startedAt !== this.generation || !saved2) return false;
|
|
832
|
-
this.cachedLicense = saved2;
|
|
833
|
-
this.applyTier("free");
|
|
834
|
-
this.state.isValidating = false;
|
|
835
|
-
this.state.expiresAt = null;
|
|
836
|
-
this.setVerifiedAt(now2);
|
|
837
|
-
this.emit({ type: "validated", isPro: false });
|
|
838
|
-
return true;
|
|
839
|
-
}
|
|
840
|
-
if (isFatalLicenseError(result.code) || !isAcceptedValidation(response)) {
|
|
841
|
-
this.lock();
|
|
842
|
-
this.state.isValidating = false;
|
|
843
|
-
this.state.error = result.detail || "License could not be verified";
|
|
844
|
-
this.state.errorKind = "verdict";
|
|
845
|
-
this.emit({ type: "error", isPro: false, error: this.state.error });
|
|
846
|
-
await this.clearCachedLicense();
|
|
847
|
-
await this.saveHold({
|
|
848
|
-
key: normalizedKey,
|
|
849
|
-
code: result.code,
|
|
850
|
-
detail: this.state.error,
|
|
851
|
-
verdictAt: Date.now()
|
|
852
|
-
});
|
|
853
|
-
return false;
|
|
854
|
-
}
|
|
855
|
-
if (!response.data) {
|
|
856
|
-
this.lock();
|
|
857
|
-
this.state.isValidating = false;
|
|
858
|
-
this.state.error = result.detail || "License not found";
|
|
859
|
-
this.state.errorKind = "verdict";
|
|
860
|
-
this.emit({ type: "error", isPro: false, error: this.state.error });
|
|
861
|
-
await this.clearCachedLicense();
|
|
862
|
-
return false;
|
|
863
|
-
}
|
|
864
|
-
const tier = resolveTier(response.data);
|
|
865
|
-
const licenseId = response.data.id;
|
|
866
|
-
const now = Date.now();
|
|
867
|
-
const fleet = machineCount(response) >= LICENSE_FLEET_MACHINE_THRESHOLD;
|
|
868
|
-
const devBundle = isDevBundle();
|
|
869
|
-
const canRegister = devBundle && !fleet;
|
|
870
|
-
const quiet = tier === "free" && (fleet || !devBundle);
|
|
871
|
-
let fingerprint = this.cachedLicense?.fingerprint;
|
|
872
|
-
let deviceName = this.cachedLicense?.deviceName;
|
|
873
|
-
let needsRegistration = false;
|
|
874
|
-
if (!fingerprint) {
|
|
875
|
-
fingerprint = await boundedStorage(this.getOrCreateDeviceFingerprint(), generateUUID());
|
|
876
|
-
if (startedAt !== this.generation) return false;
|
|
877
|
-
deviceName = getDefaultDeviceName();
|
|
878
|
-
needsRegistration = canRegister;
|
|
879
|
-
}
|
|
880
|
-
let deferCaching = false;
|
|
881
|
-
if (needsRegistration) {
|
|
882
|
-
if (this.deviceRegistrationCallback) {
|
|
883
|
-
this.pendingRegistration = {
|
|
884
|
-
licenseId,
|
|
885
|
-
licenseKey: normalizedKey,
|
|
886
|
-
fingerprint
|
|
887
|
-
};
|
|
888
|
-
this.deviceRegistrationCallback({
|
|
889
|
-
fingerprint,
|
|
890
|
-
defaultName: deviceName
|
|
891
|
-
});
|
|
892
|
-
deferCaching = true;
|
|
893
|
-
} else {
|
|
894
|
-
this.registerDeviceAsync(licenseId, normalizedKey, fingerprint, deviceName);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
const cached = {
|
|
898
|
-
licenseKey: normalizedKey,
|
|
899
|
-
licenseId,
|
|
900
|
-
valid: true,
|
|
901
|
-
isPro: tier === "pro",
|
|
902
|
-
tier,
|
|
903
|
-
expiresAt: response.data.attributes.expiry ?? null,
|
|
904
|
-
cachedAt: now,
|
|
905
|
-
lastValidatedAt: now,
|
|
906
|
-
// Don't cache fingerprint if we're deferring to UI for registration
|
|
907
|
-
fingerprint: deferCaching ? void 0 : fingerprint,
|
|
908
|
-
deviceName: deferCaching ? void 0 : deviceName,
|
|
909
|
-
quiet: quiet || void 0
|
|
910
|
-
};
|
|
911
|
-
const saved = await this.saveCache(cached, startedAt);
|
|
912
|
-
if (startedAt !== this.generation || !saved) return false;
|
|
913
|
-
this.cachedLicense = saved;
|
|
914
|
-
this.applyTier(tier);
|
|
915
|
-
this.state.isValidating = false;
|
|
916
|
-
this.state.expiresAt = cached.expiresAt ? new Date(cached.expiresAt) : null;
|
|
917
|
-
this.setVerifiedAt(now);
|
|
918
|
-
if (this.hold) await this.saveHold(null);
|
|
919
|
-
if (startedAt !== this.generation) return false;
|
|
920
|
-
this.retryAttempt = 0;
|
|
921
|
-
this.emit({ type: "validated", isPro: this.state.isPro });
|
|
922
|
-
return true;
|
|
923
|
-
} catch (error) {
|
|
924
|
-
const errorMessage = error instanceof Error ? error.message : "Validation failed";
|
|
925
|
-
if (startedAt !== this.generation) return false;
|
|
926
|
-
this.state.isValidating = false;
|
|
927
|
-
this.state.error = errorMessage;
|
|
928
|
-
this.state.errorKind = "verdict";
|
|
929
|
-
this.emit({ type: "error", isPro: false, error: errorMessage });
|
|
930
|
-
return false;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Retry a transiently-failed validation on a short, finite backoff.
|
|
935
|
-
*
|
|
936
|
-
* Only ever scheduled when there was nothing cached to fall back on, which
|
|
937
|
-
* is the one case that leaves a real customer stuck: the key comes in as a
|
|
938
|
-
* prop, so without this the next attempt is the daily revalidation timer.
|
|
939
|
-
*/
|
|
940
|
-
scheduleRetry(licenseKey) {
|
|
941
|
-
this.cancelRetry();
|
|
942
|
-
const delay = LICENSE_TRANSIENT_RETRY_DELAYS[this.retryAttempt];
|
|
943
|
-
if (delay === void 0) return;
|
|
944
|
-
this.retryAttempt += 1;
|
|
945
|
-
const startedAt = this.generation;
|
|
946
|
-
this.retryTimer = setTimeout(() => {
|
|
947
|
-
this.retryTimer = null;
|
|
948
|
-
if (startedAt !== this.generation) return;
|
|
949
|
-
if (this.state.isValidating) return;
|
|
950
|
-
void this.setLicenseKey(licenseKey, { force: true, isRetry: true });
|
|
951
|
-
}, delay);
|
|
952
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
953
|
-
globalThis.__LICENSE_MANAGER_RETRY_TIMER__ = this.retryTimer;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
/**
|
|
957
|
-
* Admit (or reject) a free-tier token. Verification is a signature check
|
|
958
|
-
* against the public key in config.ts; the cache is written so the next
|
|
959
|
-
* launch and the account-admission clock see a validated key, and marked
|
|
960
|
-
* quiet so nothing ever revalidates it against a server.
|
|
961
|
-
*/
|
|
962
|
-
async admitToken(token, startedAt) {
|
|
963
|
-
const claims = verifyFreeToken(token);
|
|
964
|
-
if (startedAt !== this.generation) return false;
|
|
965
|
-
if (!claims) {
|
|
966
|
-
this.lock();
|
|
967
|
-
this.state.isValidating = false;
|
|
968
|
-
this.state.error = "This key is not valid. Check that it was copied completely.";
|
|
969
|
-
this.state.errorKind = "verdict";
|
|
970
|
-
this.emit({ type: "error", isPro: false, error: this.state.error });
|
|
971
|
-
await this.clearCachedLicense();
|
|
972
|
-
return false;
|
|
973
|
-
}
|
|
974
|
-
const now = Date.now();
|
|
975
|
-
const fingerprint = this.cachedLicense?.fingerprint ?? await boundedStorage(this.getOrCreateDeviceFingerprint(), generateUUID());
|
|
976
|
-
if (startedAt !== this.generation) return false;
|
|
977
|
-
const saved = await this.saveCache({
|
|
978
|
-
licenseKey: token,
|
|
979
|
-
licenseId: `token:${claims.accountHash}`,
|
|
980
|
-
valid: true,
|
|
981
|
-
isPro: false,
|
|
982
|
-
tier: "free",
|
|
983
|
-
expiresAt: null,
|
|
984
|
-
cachedAt: now,
|
|
985
|
-
lastValidatedAt: now,
|
|
986
|
-
fingerprint,
|
|
987
|
-
deviceName: this.cachedLicense?.deviceName ?? getDefaultDeviceName(),
|
|
988
|
-
quiet: true
|
|
989
|
-
}, startedAt);
|
|
990
|
-
if (startedAt !== this.generation || !saved) return false;
|
|
991
|
-
this.cachedLicense = saved;
|
|
992
|
-
this.applyTier("free");
|
|
993
|
-
this.state.isValidating = false;
|
|
994
|
-
this.state.expiresAt = null;
|
|
995
|
-
this.setVerifiedAt(now);
|
|
996
|
-
this.retryAttempt = 0;
|
|
997
|
-
this.emit({ type: "validated", isPro: false });
|
|
998
|
-
return true;
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* "Don't ask right now": serve the trusted cache if it vouches for this
|
|
1002
|
-
* key, otherwise record why the tier can't move, then try exactly once when
|
|
1003
|
-
* the wait is over. Used for server-set backoffs, both fresh and remembered.
|
|
1004
|
-
*/
|
|
1005
|
-
standDown(licenseKey, startedAt, reason, wait) {
|
|
1006
|
-
if (startedAt !== this.generation) return false;
|
|
1007
|
-
this.state.isValidating = false;
|
|
1008
|
-
const cached = this.cachedLicense;
|
|
1009
|
-
if (cached && cached.licenseKey === licenseKey && verifyCacheSignature(cached) && this.isCacheValid(cached)) {
|
|
1010
|
-
this.applyTier(tierOfCache(cached));
|
|
1011
|
-
this.state.error = null;
|
|
1012
|
-
this.state.errorKind = null;
|
|
1013
|
-
this.state.expiresAt = cached.expiresAt ? new Date(cached.expiresAt) : null;
|
|
1014
|
-
this.setVerifiedAt(cached.cachedAt);
|
|
1015
|
-
if (!this.isPro()) this.scheduleRetryIn(licenseKey, wait);
|
|
1016
|
-
this.emit({ type: "validated", isPro: this.state.isPro });
|
|
1017
|
-
return true;
|
|
1018
|
-
}
|
|
1019
|
-
this.state.error = reason;
|
|
1020
|
-
this.state.errorKind = "transport";
|
|
1021
|
-
this.scheduleRetryIn(licenseKey, wait);
|
|
1022
|
-
this.emit({ type: "error", isPro: this.state.isPro, error: reason });
|
|
1023
|
-
return false;
|
|
1024
|
-
}
|
|
1025
|
-
/** One retry at a server-chosen time. Replaces any ladder step pending. */
|
|
1026
|
-
scheduleRetryIn(licenseKey, delay) {
|
|
1027
|
-
this.cancelRetry();
|
|
1028
|
-
const startedAt = this.generation;
|
|
1029
|
-
this.retryTimer = setTimeout(() => {
|
|
1030
|
-
this.retryTimer = null;
|
|
1031
|
-
if (startedAt !== this.generation) return;
|
|
1032
|
-
if (this.state.isValidating) return;
|
|
1033
|
-
void this.setLicenseKey(licenseKey, { force: true, isRetry: true });
|
|
1034
|
-
}, Math.max(1, Math.min(delay, 2147483647)));
|
|
1035
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) devTimers.__LICENSE_MANAGER_RETRY_TIMER__ = this.retryTimer;
|
|
1036
|
-
}
|
|
1037
|
-
/** Drop any pending retry — a newer decision has replaced it. */
|
|
1038
|
-
cancelRetry() {
|
|
1039
|
-
if (this.retryTimer) {
|
|
1040
|
-
clearTimeout(this.retryTimer);
|
|
1041
|
-
this.retryTimer = null;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
/**
|
|
1045
|
-
* Clear the current license
|
|
1046
|
-
*/
|
|
1047
|
-
async clearLicense() {
|
|
1048
|
-
this.generation += 1;
|
|
1049
|
-
this.cancelRetry();
|
|
1050
|
-
this.retryAttempt = 0;
|
|
1051
|
-
this.configuredKey = null;
|
|
1052
|
-
this.state.licenseKey = null;
|
|
1053
|
-
this.lock();
|
|
1054
|
-
this.state.expiresAt = null;
|
|
1055
|
-
this.state.error = null;
|
|
1056
|
-
this.state.errorKind = null;
|
|
1057
|
-
this.cachedLicense = null;
|
|
1058
|
-
this.pendingRegistration = null;
|
|
1059
|
-
this.state.isValidating = false;
|
|
1060
|
-
this.emit({ type: "deactivated", isPro: false });
|
|
1061
|
-
await this.clearCachedLicense();
|
|
1062
|
-
if (this.hold?.verdictAt !== void 0) {
|
|
1063
|
-
await this.saveHold(this.hold.backoffUntil ? { backoffUntil: this.hold.backoffUntil } : null);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Set the tier a validated key granted and keep the derived fields in step.
|
|
1068
|
-
*
|
|
1069
|
-
* `isPro` is a mirror of `tier === "pro"` and must never be assigned
|
|
1070
|
-
* directly — that is exactly how a free key would end up granting Pro.
|
|
1071
|
-
*/
|
|
1072
|
-
applyTier(tier) {
|
|
1073
|
-
this.state.tier = tier;
|
|
1074
|
-
this.state.tierSource = "key";
|
|
1075
|
-
this.state.isPro = tier === "pro";
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Forget whatever the current key had proven. There is no tier for "nothing
|
|
1079
|
-
* validated": the tier falls back to free and `tierSource: "none"` is what
|
|
1080
|
-
* makes `getAccountAccess()` report the manager locked.
|
|
1081
|
-
*/
|
|
1082
|
-
lock() {
|
|
1083
|
-
this.state.tier = "free";
|
|
1084
|
-
this.state.tierSource = "none";
|
|
1085
|
-
this.state.isPro = false;
|
|
1086
|
-
this.verifiedAt = null;
|
|
1087
|
-
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
1088
|
-
this.expiryTimer = null;
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* The current tier. This is what caps should read; whether the tools are
|
|
1092
|
-
* usable at all is `getAccountAccess()`.
|
|
1093
|
-
*/
|
|
1094
|
-
getTier() {
|
|
1095
|
-
if (!this.hasAccountAccess()) return "free";
|
|
1096
|
-
if (this.state.expiresAt && Date.now() >= this.state.expiresAt.getTime()) return "free";
|
|
1097
|
-
return this.state.tier;
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Check if the user has Pro features
|
|
1101
|
-
*/
|
|
1102
|
-
isPro() {
|
|
1103
|
-
return this.getTier() === "pro";
|
|
1104
|
-
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Check if a specific entitlement is available
|
|
1107
|
-
*/
|
|
1108
|
-
hasEntitlement(code) {
|
|
1109
|
-
return this.isPro();
|
|
1110
|
-
}
|
|
1111
|
-
/**
|
|
1112
|
-
* Get current license state
|
|
1113
|
-
*/
|
|
1114
|
-
getState() {
|
|
1115
|
-
const access = this.getAccountAccess();
|
|
1116
|
-
const tier = this.getTier();
|
|
1117
|
-
return { ...this.state, tier, isPro: tier === "pro", access };
|
|
1118
|
-
}
|
|
1119
|
-
getAccountAccess() {
|
|
1120
|
-
return resolveAccountAccess({
|
|
1121
|
-
initialized: this.state.isInitialized,
|
|
1122
|
-
validating: this.state.isValidating,
|
|
1123
|
-
key: this.state.licenseKey,
|
|
1124
|
-
tierSource: this.state.tierSource,
|
|
1125
|
-
verifiedAt: this.verifiedAt
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
hasAccountAccess() {
|
|
1129
|
-
return this.getAccountAccess().status === "admitted";
|
|
1130
|
-
}
|
|
1131
|
-
setVerifiedAt(at) {
|
|
1132
|
-
this.verifiedAt = at;
|
|
1133
|
-
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
1134
|
-
const accountDeadline = at + LICENSE_CACHE_DURATION;
|
|
1135
|
-
const paidDeadline = this.state.expiresAt?.getTime();
|
|
1136
|
-
const nextDeadline = paidDeadline && paidDeadline > Date.now() ? Math.min(accountDeadline, paidDeadline) : accountDeadline;
|
|
1137
|
-
const remaining = nextDeadline - Date.now();
|
|
1138
|
-
this.expiryTimer = setTimeout(() => {
|
|
1139
|
-
this.expiryTimer = null;
|
|
1140
|
-
if (this.verifiedAt !== at) return;
|
|
1141
|
-
this.emit({ type: "validated", isPro: this.isPro() });
|
|
1142
|
-
if (Date.now() < accountDeadline) this.setVerifiedAt(at);
|
|
1143
|
-
}, Math.max(1, Math.min(remaining, 2147483647)));
|
|
1144
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) devTimers.__LICENSE_MANAGER_EXPIRY_TIMER__ = this.expiryTimer;
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Subscribe to license events
|
|
1148
|
-
*/
|
|
1149
|
-
subscribe(listener) {
|
|
1150
|
-
this.listeners.add(listener);
|
|
1151
|
-
return () => this.listeners.delete(listener);
|
|
1152
|
-
}
|
|
1153
|
-
/**
|
|
1154
|
-
* Subscribe to device registration events
|
|
1155
|
-
* When a new device needs to be registered, the callback is invoked
|
|
1156
|
-
* with the fingerprint and default name suggestion.
|
|
1157
|
-
* Call completeDeviceRegistration() to finish registration with user's chosen name.
|
|
1158
|
-
*/
|
|
1159
|
-
onDeviceRegistrationNeeded(callback) {
|
|
1160
|
-
this.deviceRegistrationCallback = callback;
|
|
1161
|
-
return () => {
|
|
1162
|
-
this.deviceRegistrationCallback = null;
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
/**
|
|
1166
|
-
* Complete device registration with user-provided name
|
|
1167
|
-
* Call this after onDeviceRegistrationNeeded callback to finish registration
|
|
1168
|
-
*/
|
|
1169
|
-
async completeDeviceRegistration(deviceName) {
|
|
1170
|
-
if (!this.pendingRegistration) {
|
|
1171
|
-
console.warn("[Buoy] No pending device registration");
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
const { licenseId, licenseKey, fingerprint } = this.pendingRegistration;
|
|
1175
|
-
const startedAt = this.generation;
|
|
1176
|
-
const name = deviceName.trim() || getDefaultDeviceName();
|
|
1177
|
-
const metadata = getDeviceMetadata();
|
|
1178
|
-
try {
|
|
1179
|
-
await activateMachine(licenseId, licenseKey, fingerprint, name, metadata);
|
|
1180
|
-
} catch (error) {
|
|
1181
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1182
|
-
if (errorMessage === "FINGERPRINT_TAKEN" || errorMessage === "MACHINE_LIMIT_EXCEEDED") {
|
|
1183
|
-
} else {
|
|
1184
|
-
console.warn("[Buoy] Device registration failed:", errorMessage);
|
|
1185
|
-
throw error;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
if (startedAt !== this.generation) return;
|
|
1189
|
-
if (this.cachedLicense?.licenseKey === licenseKey) {
|
|
1190
|
-
const saved = await this.saveCache({ ...this.cachedLicense, fingerprint, deviceName: name }, startedAt);
|
|
1191
|
-
if (startedAt !== this.generation) return;
|
|
1192
|
-
if (saved) this.cachedLicense = saved;
|
|
1193
|
-
}
|
|
1194
|
-
this.pendingRegistration = null;
|
|
1195
|
-
}
|
|
1196
|
-
// Private methods
|
|
1197
|
-
emit(event) {
|
|
1198
|
-
const listeners = [...this.listeners];
|
|
1199
|
-
listeners.forEach((listener) => {
|
|
1200
|
-
try {
|
|
1201
|
-
listener(event);
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
console.warn("[React Buoy] License listener error:", error);
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
isCacheValid(cached) {
|
|
1208
|
-
const now = Date.now();
|
|
1209
|
-
const cacheAge = now - cached.cachedAt;
|
|
1210
|
-
if (!cached.valid || !Number.isFinite(cached.cachedAt) || cacheAge < 0 || cacheAge >= LICENSE_CACHE_DURATION) {
|
|
1211
|
-
return false;
|
|
1212
|
-
}
|
|
1213
|
-
return true;
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Whether the cache was validated against Keygen recently enough that a
|
|
1217
|
-
* launch-time revalidation would add nothing (see LICENSE_LAUNCH_VALIDATION_TTL).
|
|
1218
|
-
*/
|
|
1219
|
-
isCacheFresh(cached) {
|
|
1220
|
-
const ttl = this.isQuiet(cached) ? LICENSE_QUIET_VALIDATION_TTL : LICENSE_LAUNCH_VALIDATION_TTL;
|
|
1221
|
-
return Date.now() - (cached.lastValidatedAt ?? 0) < ttl;
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Whether a cache has anything to learn before it expires. Blobs written
|
|
1225
|
-
* before the flag existed are judged the same way they would be today.
|
|
1226
|
-
*/
|
|
1227
|
-
isQuiet(cached) {
|
|
1228
|
-
if (cached.quiet !== void 0) return cached.quiet;
|
|
1229
|
-
return tierOfCache(cached) === "free" && !isDevBundle();
|
|
1230
|
-
}
|
|
1231
|
-
async loadHold() {
|
|
1232
|
-
if (this.hold !== void 0) return this.hold;
|
|
1233
|
-
try {
|
|
1234
|
-
const storage2 = await getLicenseStorage();
|
|
1235
|
-
const raw = await storage2.getItem(LICENSE_HOLD_STORAGE_KEY);
|
|
1236
|
-
const parsed = raw ? JSON.parse(raw) : null;
|
|
1237
|
-
this.hold = parsed && typeof parsed === "object" ? parsed : null;
|
|
1238
|
-
} catch {
|
|
1239
|
-
this.hold = null;
|
|
1240
|
-
}
|
|
1241
|
-
return this.hold;
|
|
1242
|
-
}
|
|
1243
|
-
/** Replace the hold record (null clears it). Never throws: a failed write only costs a request. */
|
|
1244
|
-
async saveHold(record) {
|
|
1245
|
-
this.hold = record;
|
|
1246
|
-
try {
|
|
1247
|
-
const storage2 = await getLicenseStorage();
|
|
1248
|
-
if (record) await storage2.setItem(LICENSE_HOLD_STORAGE_KEY, JSON.stringify(record));
|
|
1249
|
-
else await storage2.removeItem(LICENSE_HOLD_STORAGE_KEY);
|
|
1250
|
-
} catch {
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
/**
|
|
1254
|
-
* Write the cache with a fresh checksum. Every write goes through here so a
|
|
1255
|
-
* blob can never land on disk unsigned — an unsigned blob is treated as
|
|
1256
|
-
* untrusted on the next read, which would silently drop a paying customer.
|
|
1257
|
-
*/
|
|
1258
|
-
async persistCache(cache) {
|
|
1259
|
-
const signed = signCache(cache);
|
|
1260
|
-
const storage2 = await getLicenseStorage();
|
|
1261
|
-
await storage2.setItem(LICENSE_STORAGE_KEY, JSON.stringify(signed));
|
|
1262
|
-
return signed;
|
|
1263
|
-
}
|
|
1264
|
-
saveCache(cache, startedAt) {
|
|
1265
|
-
const operation = this.persistence.catch(() => {
|
|
1266
|
-
}).then(async () => {
|
|
1267
|
-
if (startedAt !== this.generation) return null;
|
|
1268
|
-
const saved = await this.persistCache(cache);
|
|
1269
|
-
return startedAt === this.generation ? saved : null;
|
|
1270
|
-
});
|
|
1271
|
-
this.persistence = operation;
|
|
1272
|
-
return boundedStorage(operation, signCache(cache));
|
|
1273
|
-
}
|
|
1274
|
-
async loadCachedLicense() {
|
|
1275
|
-
try {
|
|
1276
|
-
await this.persistence.catch(() => {
|
|
1277
|
-
});
|
|
1278
|
-
const storage2 = await getLicenseStorage();
|
|
1279
|
-
const data = await storage2.getItem(LICENSE_STORAGE_KEY);
|
|
1280
|
-
if (!data) return null;
|
|
1281
|
-
const cached = JSON.parse(data);
|
|
1282
|
-
return cached;
|
|
1283
|
-
} catch (error) {
|
|
1284
|
-
console.warn("[Buoy] Error loading cached license:", error);
|
|
1285
|
-
return null;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* The install's stable device fingerprint, created once and persisted under
|
|
1290
|
-
* its own key. Older versions kept the fingerprint only inside the license
|
|
1291
|
-
* blob, so anything that cleared the blob gave the device a new identity —
|
|
1292
|
-
* this adopts a pre-existing blob fingerprint on first read so upgrading
|
|
1293
|
-
* doesn't mint a second machine record for the same install.
|
|
1294
|
-
*/
|
|
1295
|
-
getOrCreateDeviceFingerprint() {
|
|
1296
|
-
if (!this.fingerprintPromise) {
|
|
1297
|
-
this.fingerprintPromise = (async () => {
|
|
1298
|
-
const storage2 = await getLicenseStorage();
|
|
1299
|
-
const existing = await storage2.getItem(DEVICE_FINGERPRINT_STORAGE_KEY);
|
|
1300
|
-
if (existing) return existing;
|
|
1301
|
-
const fingerprint = this.cachedLicense?.fingerprint || generateUUID();
|
|
1302
|
-
await storage2.setItem(DEVICE_FINGERPRINT_STORAGE_KEY, fingerprint);
|
|
1303
|
-
return fingerprint;
|
|
1304
|
-
})();
|
|
1305
|
-
}
|
|
1306
|
-
return this.fingerprintPromise;
|
|
1307
|
-
}
|
|
1308
|
-
async clearCachedLicense() {
|
|
1309
|
-
this.cachedLicense = null;
|
|
1310
|
-
const operation = this.persistence.catch(() => {
|
|
1311
|
-
}).then(async () => {
|
|
1312
|
-
const storage2 = await getLicenseStorage();
|
|
1313
|
-
await storage2.removeItem(LICENSE_STORAGE_KEY);
|
|
1314
|
-
});
|
|
1315
|
-
this.persistence = operation;
|
|
1316
|
-
await boundedStorage(operation.catch(() => {
|
|
1317
|
-
}), void 0);
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Register device in background for visibility (non-blocking)
|
|
1321
|
-
* Failures don't affect Pro status - this is just for analytics
|
|
1322
|
-
*/
|
|
1323
|
-
registerDeviceAsync(licenseId, licenseKey, fingerprint, deviceName) {
|
|
1324
|
-
const metadata = getDeviceMetadata();
|
|
1325
|
-
activateMachine(licenseId, licenseKey, fingerprint, deviceName, metadata).catch((error) => {
|
|
1326
|
-
if (error.message !== "FINGERPRINT_TAKEN" && error.message !== "MACHINE_LIMIT_EXCEEDED") {
|
|
1327
|
-
console.warn("[Buoy] Device registration failed (non-blocking):", error.message);
|
|
1328
|
-
}
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
validateInBackground(licenseKey) {
|
|
1332
|
-
void this.setLicenseKey(licenseKey, { force: true, isRetry: true });
|
|
1333
|
-
}
|
|
1334
|
-
refreshAccess() {
|
|
1335
|
-
this.emit({ type: "validated", isPro: this.isPro() });
|
|
1336
|
-
}
|
|
1337
|
-
startRevalidationTimer() {
|
|
1338
|
-
if (!this.removeForegroundListener) {
|
|
1339
|
-
this.removeForegroundListener = subscribeForeground(() => this.refreshAccess());
|
|
1340
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) devTimers.__LICENSE_MANAGER_FOREGROUND_OFF__ = this.removeForegroundListener;
|
|
1341
|
-
}
|
|
1342
|
-
if (this.revalidationTimer) {
|
|
1343
|
-
clearInterval(this.revalidationTimer);
|
|
1344
|
-
}
|
|
1345
|
-
this.revalidationTimer = setInterval(() => {
|
|
1346
|
-
if (!this.state.licenseKey || this.state.isValidating) return;
|
|
1347
|
-
const cached = this.cachedLicense;
|
|
1348
|
-
if (cached && cached.licenseKey === this.state.licenseKey && this.isCacheFresh(cached)) return;
|
|
1349
|
-
this.validateInBackground(this.state.licenseKey);
|
|
1350
|
-
}, LICENSE_REVALIDATION_INTERVAL);
|
|
1351
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
1352
|
-
globalThis.__LICENSE_MANAGER_TIMER__ = this.revalidationTimer;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
/**
|
|
1356
|
-
* Stop the revalidation timer
|
|
1357
|
-
*/
|
|
1358
|
-
stopRevalidationTimer() {
|
|
1359
|
-
this.removeForegroundListener?.();
|
|
1360
|
-
this.removeForegroundListener = null;
|
|
1361
|
-
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
1362
|
-
this.expiryTimer = null;
|
|
1363
|
-
if (this.revalidationTimer) {
|
|
1364
|
-
clearInterval(this.revalidationTimer);
|
|
1365
|
-
this.revalidationTimer = null;
|
|
1366
|
-
}
|
|
1367
|
-
this.cancelRetry();
|
|
1368
|
-
}
|
|
1369
|
-
/**
|
|
1370
|
-
* Force re-registration by clearing cached fingerprint
|
|
1371
|
-
* (For debugging/testing - forces device to re-register on next validation)
|
|
1372
|
-
*/
|
|
1373
|
-
async forceReregister() {
|
|
1374
|
-
if (this.cachedLicense) {
|
|
1375
|
-
this.cachedLicense.fingerprint = void 0;
|
|
1376
|
-
this.cachedLicense.deviceName = void 0;
|
|
1377
|
-
}
|
|
1378
|
-
this.fingerprintPromise = null;
|
|
1379
|
-
await this.clearCachedLicense();
|
|
1380
|
-
try {
|
|
1381
|
-
const storage2 = await getLicenseStorage();
|
|
1382
|
-
await storage2.removeItem(DEVICE_FINGERPRINT_STORAGE_KEY);
|
|
1383
|
-
} catch {
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
};
|
|
1387
|
-
var LicenseManager = new LicenseManagerImpl();
|
|
1388
|
-
var devTimers = globalThis;
|
|
1389
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
1390
|
-
devTimers.__LICENSE_MANAGER_FOREGROUND_OFF__?.();
|
|
1391
|
-
devTimers.__LICENSE_MANAGER_FOREGROUND_OFF__ = void 0;
|
|
1392
|
-
if (devTimers.__LICENSE_MANAGER_EXPIRY_TIMER__) clearTimeout(devTimers.__LICENSE_MANAGER_EXPIRY_TIMER__);
|
|
1393
|
-
if (devTimers.__LICENSE_MANAGER_TIMER__) {
|
|
1394
|
-
clearInterval(devTimers.__LICENSE_MANAGER_TIMER__);
|
|
1395
|
-
}
|
|
1396
|
-
if (devTimers.__LICENSE_MANAGER_RETRY_TIMER__) {
|
|
1397
|
-
clearTimeout(devTimers.__LICENSE_MANAGER_RETRY_TIMER__);
|
|
1398
|
-
devTimers.__LICENSE_MANAGER_RETRY_TIMER__ = void 0;
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
export {
|
|
1403
|
-
LicenseTransportError,
|
|
1404
|
-
isTransientLicenseError,
|
|
1405
|
-
isBuoyToken,
|
|
1406
|
-
resolveAccountAccess,
|
|
1407
|
-
LicenseManager
|
|
1408
|
-
};
|
|
1409
|
-
/*!
|
|
1410
|
-
* PROPRIETARY — Buoy Pro license enforcement. NOT open source.
|
|
1411
|
-
*
|
|
1412
|
-
* Copyright (c) Buoy. All rights reserved. Licensed under the Buoy EULA
|
|
1413
|
-
* (https://buoy.gg/terms), not an OSS license.
|
|
1414
|
-
*
|
|
1415
|
-
* This file configures the technological measure that controls access to paid
|
|
1416
|
-
* Buoy Pro features. Modifying, patching, aliasing, stubbing, or otherwise
|
|
1417
|
-
* altering it — including repointing `apiUrl` at a stand-in license server — so
|
|
1418
|
-
* that Pro features unlock without a valid license is a breach of the EULA and,
|
|
1419
|
-
* in the United States, circumvention of an access control under 17 U.S.C.
|
|
1420
|
-
* § 1201, which applies whether or not any code is copied.
|
|
1421
|
-
*
|
|
1422
|
-
* Pro is $9/mo, and is free every weekend for anyone holding a key —
|
|
1423
|
-
* including a free one (`npx buoy login`). Buy a key at
|
|
1424
|
-
* https://buoy.gg or use it free on Saturday and Sunday.
|
|
1425
|
-
*/
|
|
1426
|
-
/*!
|
|
1427
|
-
* PROPRIETARY — Buoy Pro license enforcement. NOT open source.
|
|
1428
|
-
*
|
|
1429
|
-
* Copyright (c) Buoy. All rights reserved. Licensed under the Buoy EULA
|
|
1430
|
-
* (https://buoy.gg/terms), not an OSS license.
|
|
1431
|
-
*
|
|
1432
|
-
* This file is part of the technological measure that controls access to paid
|
|
1433
|
-
* Buoy Pro features. Modifying, patching, aliasing, stubbing, or otherwise
|
|
1434
|
-
* altering it so that Pro features unlock without a valid license is a breach
|
|
1435
|
-
* of the EULA and, in the United States, circumvention of an access control
|
|
1436
|
-
* under 17 U.S.C. § 1201 — which applies whether or not any code is copied.
|
|
1437
|
-
*
|
|
1438
|
-
* Pro is $9/mo, and is free every weekend for anyone holding a key —
|
|
1439
|
-
* including a free one (`npx buoy login`). Buy a key at
|
|
1440
|
-
* https://buoy.gg or use it free on Saturday and Sunday.
|
|
1441
|
-
*/
|
|
1442
|
-
/*!
|
|
1443
|
-
* PROPRIETARY — Buoy Pro license enforcement. NOT open source.
|
|
1444
|
-
*
|
|
1445
|
-
* Copyright (c) Buoy. All rights reserved. Licensed under the Buoy EULA
|
|
1446
|
-
* (https://buoy.gg/terms), not an OSS license.
|
|
1447
|
-
*/
|
|
1448
|
-
/*!
|
|
1449
|
-
* PROPRIETARY — Buoy Pro license enforcement. NOT open source.
|
|
1450
|
-
*
|
|
1451
|
-
* Copyright (c) Buoy. All rights reserved. Licensed under the Buoy EULA
|
|
1452
|
-
* (https://buoy.gg/terms), not an OSS license.
|
|
1453
|
-
*
|
|
1454
|
-
* This file is part of the technological measure that controls access to paid
|
|
1455
|
-
* Buoy Pro features: it decides which tier a validated key grants. Modifying,
|
|
1456
|
-
* patching, aliasing, stubbing, or otherwise altering it so that Pro features
|
|
1457
|
-
* unlock without a valid license is a breach of the EULA and, in the United
|
|
1458
|
-
* States, circumvention of an access control under 17 U.S.C. § 1201 — which
|
|
1459
|
-
* applies whether or not any code is copied.
|
|
1460
|
-
*
|
|
1461
|
-
* Pro is $9/mo, and is free every weekend for anyone holding a key —
|
|
1462
|
-
* including a free one (`npx buoy login`). Buy a key at
|
|
1463
|
-
* https://buoy.gg or use it free on Saturday and Sunday.
|
|
1464
|
-
*/
|
|
1465
|
-
/*!
|
|
1466
|
-
* PROPRIETARY — Buoy Pro license enforcement. NOT open source.
|
|
1467
|
-
*
|
|
1468
|
-
* Copyright (c) Buoy. All rights reserved. Licensed under the Buoy EULA
|
|
1469
|
-
* (https://buoy.gg/terms), not an OSS license.
|
|
1470
|
-
*
|
|
1471
|
-
* This file is the technological measure that controls access to paid Buoy Pro
|
|
1472
|
-
* features. Modifying, patching, aliasing, stubbing, or otherwise altering it
|
|
1473
|
-
* so that Pro features unlock without a valid license is a breach of the EULA
|
|
1474
|
-
* and, in the United States, circumvention of an access control under
|
|
1475
|
-
* 17 U.S.C. § 1201 — which applies whether or not any code is copied.
|
|
1476
|
-
*
|
|
1477
|
-
* Pro is $9/mo, and is free every weekend for anyone holding a key —
|
|
1478
|
-
* including a free one (`npx buoy login`). Buy a key at
|
|
1479
|
-
* https://buoy.gg or use it free on Saturday and Sunday.
|
|
1480
|
-
*/
|