@gorilla-engine-sdk/ge-product-hub 0.1.4
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.md +64 -0
- package/README.md +202 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/lib/clients.d.ts +1039 -0
- package/dist/lib/clients.js +2036 -0
- package/dist/lib/credentialStore.d.ts +8 -0
- package/dist/lib/credentialStore.js +80 -0
- package/dist/lib/errors.d.ts +48 -0
- package/dist/lib/errors.js +63 -0
- package/dist/lib/types.d.ts +406 -0
- package/dist/lib/types.js +58 -0
- package/package.json +48 -0
|
@@ -0,0 +1,2036 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ProductHub = exports.HARDWARE_FINGERPRINT_VERSION = exports.SIGNATURE_SEED_VERSION = void 0;
|
|
37
|
+
const errors_1 = require("./errors");
|
|
38
|
+
const types_1 = require("./types");
|
|
39
|
+
const crypto_1 = require("crypto");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const credentialStore_1 = require("./credentialStore");
|
|
43
|
+
/**
|
|
44
|
+
* @internal
|
|
45
|
+
* Product Hub API base URL.
|
|
46
|
+
* Can be overridden via the PRODUCT_HUB_BASE_URL environment variable
|
|
47
|
+
* (used by the e2e test suite to target non-production environments).
|
|
48
|
+
*/
|
|
49
|
+
const BASE_URL = process.env['PRODUCT_HUB_BASE_URL'] ?? 'https://producthub.ujam.com/api/v1';
|
|
50
|
+
/**
|
|
51
|
+
* @internal
|
|
52
|
+
* Fixed signature seed version used for cryptographic verification.
|
|
53
|
+
*/
|
|
54
|
+
exports.SIGNATURE_SEED_VERSION = 6;
|
|
55
|
+
/**
|
|
56
|
+
* @internal
|
|
57
|
+
* Hardware fingerprint version for system identification.
|
|
58
|
+
*/
|
|
59
|
+
exports.HARDWARE_FINGERPRINT_VERSION = 2;
|
|
60
|
+
/**
|
|
61
|
+
* @internal
|
|
62
|
+
* HTTP client wrapper that handles Product Hub API responses and errors.
|
|
63
|
+
*/
|
|
64
|
+
async function callApi(url, opts = {}) {
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await fetch(url, opts);
|
|
68
|
+
}
|
|
69
|
+
catch (ex) {
|
|
70
|
+
throw new errors_1.ProductHubError(`Network error: ${ex?.message || ex}`, undefined);
|
|
71
|
+
}
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
let body = undefined;
|
|
74
|
+
try {
|
|
75
|
+
body = await response.json();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* ignore parse errors */
|
|
79
|
+
}
|
|
80
|
+
const message = body?.message || response.statusText || 'Request failed';
|
|
81
|
+
if (response.status === 401) {
|
|
82
|
+
throw new errors_1.NotAuthorizedError(message, body);
|
|
83
|
+
}
|
|
84
|
+
throw new errors_1.ProductHubError(message, response.status, body);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return (await response.json());
|
|
88
|
+
}
|
|
89
|
+
catch (ex) {
|
|
90
|
+
throw new errors_1.ProductHubError(`Failed to parse JSON: ${ex?.message || ex}`, response.status);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* @internal
|
|
95
|
+
* Hardware fingerprint management utility.
|
|
96
|
+
* This singleton handles automatic hardware identification and validation.
|
|
97
|
+
* Used internally by AudioPluginClient - not for direct use.
|
|
98
|
+
*/
|
|
99
|
+
class HardwareFingerprintUtil {
|
|
100
|
+
constructor() {
|
|
101
|
+
this.fingerprint = undefined;
|
|
102
|
+
this.fingerprintOverride = undefined;
|
|
103
|
+
// Detect test environment and set test fingerprint
|
|
104
|
+
if (isTestEnvironment()) {
|
|
105
|
+
this.fingerprintOverride = 'test-hardware-fingerprint-12345';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
static getInstance() {
|
|
109
|
+
if (!HardwareFingerprintUtil.instance) {
|
|
110
|
+
HardwareFingerprintUtil.instance = new HardwareFingerprintUtil();
|
|
111
|
+
}
|
|
112
|
+
return HardwareFingerprintUtil.instance;
|
|
113
|
+
}
|
|
114
|
+
generateFingerprint() {
|
|
115
|
+
try {
|
|
116
|
+
return process
|
|
117
|
+
._linkedBinding('hwfp')
|
|
118
|
+
.Fingerprint(exports.HARDWARE_FINGERPRINT_VERSION)
|
|
119
|
+
.toString();
|
|
120
|
+
}
|
|
121
|
+
catch (ex) {
|
|
122
|
+
try {
|
|
123
|
+
return process
|
|
124
|
+
.binding('hwfp')
|
|
125
|
+
.Fingerprint(exports.HARDWARE_FINGERPRINT_VERSION)
|
|
126
|
+
.toString();
|
|
127
|
+
}
|
|
128
|
+
catch (ex2) {
|
|
129
|
+
throw new errors_1.ProductHubError('Unable to generate hardware fingerprint');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
getFingerprint() {
|
|
134
|
+
if (this.fingerprintOverride) {
|
|
135
|
+
// If we have an override, use it otherwise it's gonna fuck up our tests
|
|
136
|
+
return this.fingerprintOverride;
|
|
137
|
+
}
|
|
138
|
+
if (!this.fingerprint) {
|
|
139
|
+
// Lazy load fingerprint native module whe we are not in a testing scenario
|
|
140
|
+
this.fingerprint = this.generateFingerprint();
|
|
141
|
+
}
|
|
142
|
+
return this.fingerprint;
|
|
143
|
+
}
|
|
144
|
+
setFingerprintOverride(fingerprint) {
|
|
145
|
+
this.fingerprintOverride = fingerprint;
|
|
146
|
+
}
|
|
147
|
+
validate(fingerprint) {
|
|
148
|
+
// If we have an override, we can't use native validation - just compare strings
|
|
149
|
+
if (this.fingerprintOverride) {
|
|
150
|
+
return fingerprint === this.fingerprintOverride;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const hwfp = process._linkedBinding?.('hwfp') || process.binding?.('hwfp');
|
|
154
|
+
return hwfp?.validate ? hwfp.validate(fingerprint) : fingerprint === this.getFingerprint();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return fingerprint === this.getFingerprint();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Gorilla Engine Product Hub Client Library
|
|
163
|
+
*
|
|
164
|
+
* Provides comprehensive audio plugin licensing with
|
|
165
|
+
* activation management & trial handling.
|
|
166
|
+
*/
|
|
167
|
+
var ProductHub;
|
|
168
|
+
(function (ProductHub) {
|
|
169
|
+
/**
|
|
170
|
+
* @internal
|
|
171
|
+
* Low-level API client for Product Hub communication.
|
|
172
|
+
* This class is used internally and should not be used directly.
|
|
173
|
+
* Use AudioPluginClient instead for a complete audio plugin licensing solution.
|
|
174
|
+
*/
|
|
175
|
+
class APIClient {
|
|
176
|
+
/**
|
|
177
|
+
* Create a low-level Product Hub API client.
|
|
178
|
+
*
|
|
179
|
+
* @param options - Product, manufacturer, and optional fingerprint settings.
|
|
180
|
+
* @throws {@link ProductHubError} If a required identifier is missing, the runtime is older
|
|
181
|
+
* than Node.js 18.3, global `fetch` is unavailable, or fingerprint generation fails.
|
|
182
|
+
* @remarks One stored login session is shared by all products from this manufacturer.
|
|
183
|
+
* `hardwareFingerprint` is intended for tests; production clients should use the Gorilla
|
|
184
|
+
* Engine hardware-fingerprinting binding.
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const apiClient = new ProductHub.APIClient({
|
|
188
|
+
* manufacturerId: 'manufacturer-id',
|
|
189
|
+
* productId: 'product-id',
|
|
190
|
+
* });
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
constructor(options) {
|
|
194
|
+
this.bearerToken = null;
|
|
195
|
+
const { manufacturerId, productId, hardwareFingerprint } = options;
|
|
196
|
+
if (!manufacturerId) {
|
|
197
|
+
throw new errors_1.ProductHubError('manufacturerId is required when constructing APIClient');
|
|
198
|
+
}
|
|
199
|
+
if (!productId) {
|
|
200
|
+
throw new errors_1.ProductHubError('productId is required when constructing APIClient');
|
|
201
|
+
}
|
|
202
|
+
this.manufacturerId = manufacturerId;
|
|
203
|
+
this.productId = productId;
|
|
204
|
+
this.hwfpUtil = HardwareFingerprintUtil.getInstance();
|
|
205
|
+
// Update fingerprint if provided
|
|
206
|
+
if (hardwareFingerprint) {
|
|
207
|
+
this.hwfpUtil.setFingerprintOverride(hardwareFingerprint);
|
|
208
|
+
}
|
|
209
|
+
// Verify Node.js version
|
|
210
|
+
this.validateNodeVersion();
|
|
211
|
+
}
|
|
212
|
+
credentialLocation() {
|
|
213
|
+
return (0, credentialStore_1.createCredentialLocation)(this.manufacturerId);
|
|
214
|
+
}
|
|
215
|
+
isTokenUsable(token) {
|
|
216
|
+
try {
|
|
217
|
+
const parts = token.split('.');
|
|
218
|
+
if (parts.length !== 3) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
222
|
+
if (payload.aud !== this.manufacturerId) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (typeof payload.exp === 'number' && payload.exp * 1000 <= Date.now()) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (payload.sub !== this.hwfpUtil.getFingerprint()) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async readPersistedToken() {
|
|
238
|
+
const location = this.credentialLocation();
|
|
239
|
+
try {
|
|
240
|
+
const token = (await (0, credentialStore_1.getCredential)(location.service, location.account))?.trim();
|
|
241
|
+
if (!token) {
|
|
242
|
+
return { token: null, status: types_1.SessionStatus.None };
|
|
243
|
+
}
|
|
244
|
+
if (!this.isTokenUsable(token)) {
|
|
245
|
+
await (0, credentialStore_1.deleteCredential)(location.service, location.account).catch(() => undefined);
|
|
246
|
+
return { token: null, status: types_1.SessionStatus.Invalid };
|
|
247
|
+
}
|
|
248
|
+
return { token, status: types_1.SessionStatus.Valid };
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return { token: null, status: types_1.SessionStatus.None };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async loadPersistedToken() {
|
|
255
|
+
return (await this.readPersistedToken()).token;
|
|
256
|
+
}
|
|
257
|
+
async persistToken(token) {
|
|
258
|
+
const location = this.credentialLocation();
|
|
259
|
+
try {
|
|
260
|
+
await (0, credentialStore_1.setCredential)(location.service, location.account, token);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Credential persistence is best-effort and must not break login flow.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Check whether a usable login session is stored for this product and device.
|
|
268
|
+
*
|
|
269
|
+
* @returns A promise resolving to `true` when a usable session exists; otherwise `false`.
|
|
270
|
+
* @remarks Malformed, expired, or wrong-scope stored tokens are removed before returning.
|
|
271
|
+
*/
|
|
272
|
+
async hasStoredSession() {
|
|
273
|
+
return (await this.loadPersistedToken()) !== null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Read non-sensitive claims from the active stored login session.
|
|
277
|
+
*
|
|
278
|
+
* @returns The stored session's email, user ID, and expiration, or `null` when no usable
|
|
279
|
+
* session exists. The bearer token is never exposed.
|
|
280
|
+
* @remarks Malformed, expired, or wrong-scope stored tokens are removed before returning.
|
|
281
|
+
*/
|
|
282
|
+
async getStoredSessionInfo() {
|
|
283
|
+
const token = await this.loadPersistedToken();
|
|
284
|
+
if (!token)
|
|
285
|
+
return null;
|
|
286
|
+
try {
|
|
287
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
|
|
288
|
+
return {
|
|
289
|
+
email: typeof payload.email === 'string' ? payload.email : null,
|
|
290
|
+
userId: typeof payload.user_id === 'string' ? payload.user_id : null,
|
|
291
|
+
expiresAt: typeof payload.exp === 'number' ? new Date(payload.exp * 1000) : null,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Clear the active login session.
|
|
300
|
+
*
|
|
301
|
+
* @returns A promise that resolves after clearing the in-memory token and attempting to
|
|
302
|
+
* remove the persisted credential.
|
|
303
|
+
* @remarks Credential-store deletion is best-effort and failures are intentionally ignored.
|
|
304
|
+
*/
|
|
305
|
+
async clearStoredSession() {
|
|
306
|
+
this.bearerToken = null;
|
|
307
|
+
const location = this.credentialLocation();
|
|
308
|
+
await (0, credentialStore_1.deleteCredential)(location.service, location.account).catch(() => undefined);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Activate the product using the active stored login session.
|
|
312
|
+
*
|
|
313
|
+
* @param serial - Optional reseller serial to claim and activate instead of using Product
|
|
314
|
+
* Hub's normal license selection.
|
|
315
|
+
* @returns The independent session status and the activation response, if one was available.
|
|
316
|
+
* A valid session with no matching activation returns `activation: null`.
|
|
317
|
+
* @throws {@link ProductHubError} If the API cannot be reached or an unexpected API error
|
|
318
|
+
* occurs. Authentication failures are returned as `SessionStatus.Invalid` instead.
|
|
319
|
+
* @remarks
|
|
320
|
+
* This method never performs a credential login. Missing sessions return
|
|
321
|
+
* `SessionStatus.None`; malformed, expired, wrong-scope, or API-rejected sessions return
|
|
322
|
+
* `SessionStatus.Invalid` and are removed. HTTP 404 and 409 responses return a valid session
|
|
323
|
+
* with no activation.
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* const result = await apiClient.activateProductWithStoredSession();
|
|
327
|
+
* if (result.sessionStatus === SessionStatus.Valid && result.activation) {
|
|
328
|
+
* console.log(result.activation.serial);
|
|
329
|
+
* }
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
async activateProductWithStoredSession(serial) {
|
|
333
|
+
const storedSession = await this.readPersistedToken();
|
|
334
|
+
if (!storedSession.token) {
|
|
335
|
+
return { sessionStatus: storedSession.status, activation: null };
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
return {
|
|
339
|
+
sessionStatus: types_1.SessionStatus.Valid,
|
|
340
|
+
activation: await this.activateProductWithToken(storedSession.token, serial),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch (ex) {
|
|
344
|
+
if (ex instanceof errors_1.NotAuthorizedError) {
|
|
345
|
+
await this.clearStoredSession();
|
|
346
|
+
return { sessionStatus: types_1.SessionStatus.Invalid, activation: null };
|
|
347
|
+
}
|
|
348
|
+
if (ex instanceof errors_1.ProductHubError && (ex.status === 404 || ex.status === 409)) {
|
|
349
|
+
return { sessionStatus: types_1.SessionStatus.Valid, activation: null };
|
|
350
|
+
}
|
|
351
|
+
throw ex;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
validateNodeVersion() {
|
|
355
|
+
const nodeVersion = globalThis?.process?.versions?.node;
|
|
356
|
+
if (!nodeVersion) {
|
|
357
|
+
throw new errors_1.ProductHubError('Unsupported runtime: Node >=18.3 is required.');
|
|
358
|
+
}
|
|
359
|
+
const [majorStr, minorStr] = nodeVersion.split('.');
|
|
360
|
+
const major = parseInt(majorStr, 10);
|
|
361
|
+
const minor = parseInt(minorStr || '0', 10);
|
|
362
|
+
if (major < 18 || (major === 18 && minor < 3)) {
|
|
363
|
+
throw new errors_1.ProductHubError(`Node >=18.3 required. Detected ${nodeVersion}. Upgrade your runtime.`);
|
|
364
|
+
}
|
|
365
|
+
if (!globalThis.fetch) {
|
|
366
|
+
throw new errors_1.ProductHubError('Global fetch not found. Node >=18.3 should provide fetch by default.');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Authenticate a user and make the returned token the active session.
|
|
371
|
+
*
|
|
372
|
+
* @param email - End-user email address.
|
|
373
|
+
* @param password - End-user password.
|
|
374
|
+
* @returns The bearer token returned by Product Hub.
|
|
375
|
+
* @throws {@link NotAuthorizedError} If the credentials are rejected.
|
|
376
|
+
* @throws {@link ProductHubError} If fingerprint generation, networking, response parsing,
|
|
377
|
+
* or another API operation fails.
|
|
378
|
+
* @remarks The token is retained in memory and persisted in the operating system's secure
|
|
379
|
+
* credential store on a best-effort basis.
|
|
380
|
+
* @example
|
|
381
|
+
* ```typescript
|
|
382
|
+
* const token = await apiClient.loginUser('user@example.com', 'secret');
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
async loginUser(email, password) {
|
|
386
|
+
const url = `${BASE_URL}/clients/any/manufacturers/${this.manufacturerId}/users/login`;
|
|
387
|
+
const hardwareFingerprint = this.hwfpUtil.getFingerprint();
|
|
388
|
+
const response = await callApi(url, {
|
|
389
|
+
method: 'POST',
|
|
390
|
+
headers: { 'Content-Type': 'application/json' },
|
|
391
|
+
body: JSON.stringify({
|
|
392
|
+
email,
|
|
393
|
+
password,
|
|
394
|
+
fingerprint: hardwareFingerprint,
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
this.bearerToken = response.token;
|
|
398
|
+
await this.persistToken(response.token);
|
|
399
|
+
return response.token;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Activate a product with a license
|
|
403
|
+
*/
|
|
404
|
+
async activateProductWithToken(bearer, serial) {
|
|
405
|
+
const hardwareFingerprint = this.hwfpUtil.getFingerprint();
|
|
406
|
+
const url = `${BASE_URL}/clients/any/products/${this.productId}/activations`;
|
|
407
|
+
return await callApi(url, {
|
|
408
|
+
method: 'POST',
|
|
409
|
+
headers: {
|
|
410
|
+
'Content-Type': 'application/json',
|
|
411
|
+
Authorization: `Bearer ${bearer}`,
|
|
412
|
+
},
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
hardware_fingerprint: hardwareFingerprint,
|
|
415
|
+
hardware_fingerprint_version: exports.HARDWARE_FINGERPRINT_VERSION,
|
|
416
|
+
signature_seed_version: exports.SIGNATURE_SEED_VERSION,
|
|
417
|
+
...(serial && { serial }),
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Authenticate with explicit credentials and activate the product.
|
|
423
|
+
*
|
|
424
|
+
* @param params - Credentials and an optional reseller serial to claim and activate.
|
|
425
|
+
* @returns The signed Product Hub activation response.
|
|
426
|
+
* @throws {@link NotAuthorizedError} If login or activation authorization fails.
|
|
427
|
+
* @throws {@link ProductHubError} If fingerprint generation, networking, response parsing,
|
|
428
|
+
* or another API operation fails.
|
|
429
|
+
* @remarks Explicit credentials always replace the active stored session. If activation is
|
|
430
|
+
* rejected as unauthorized after login, the newly stored session is cleared.
|
|
431
|
+
* @example
|
|
432
|
+
* ```typescript
|
|
433
|
+
* const activation = await apiClient.activateProduct({
|
|
434
|
+
* email: 'user@example.com',
|
|
435
|
+
* password: 'secret',
|
|
436
|
+
* serial: 'STD-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX',
|
|
437
|
+
* });
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
async activateProduct(params) {
|
|
441
|
+
const bearer = await this.loginUser(params.email, params.password);
|
|
442
|
+
try {
|
|
443
|
+
return await this.activateProductWithToken(bearer, params.serial);
|
|
444
|
+
}
|
|
445
|
+
catch (ex) {
|
|
446
|
+
if (ex instanceof errors_1.NotAuthorizedError) {
|
|
447
|
+
await this.clearStoredSession();
|
|
448
|
+
}
|
|
449
|
+
throw ex;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Activate a product using email and serial only — no login or Bearer token required.
|
|
454
|
+
*
|
|
455
|
+
* @remarks
|
|
456
|
+
* Only works when the manufacturer has "Authentication Type = None" and
|
|
457
|
+
* "Email+Serial Claiming" enabled in Product Hub. The license is claimed
|
|
458
|
+
* (if not claimed so far) and activated in a single API call.
|
|
459
|
+
*
|
|
460
|
+
* @param email - End-user email address used to identify or create the account.
|
|
461
|
+
* @param serial - License serial received after purchase.
|
|
462
|
+
* @returns The signed Product Hub activation response.
|
|
463
|
+
* @throws {@link NotAuthorizedError} If email-and-serial activation is not authorized.
|
|
464
|
+
* @throws {@link ProductHubError} If fingerprint generation, networking, response parsing,
|
|
465
|
+
* or another API operation fails.
|
|
466
|
+
* @example
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const activation = await apiClient.activateProductWithEmailAndSerial(
|
|
469
|
+
* 'user@example.com',
|
|
470
|
+
* 'STD-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX',
|
|
471
|
+
* );
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
async activateProductWithEmailAndSerial(email, serial) {
|
|
475
|
+
const hardwareFingerprint = this.hwfpUtil.getFingerprint();
|
|
476
|
+
const url = `${BASE_URL}/clients/any/products/${this.productId}/activations`;
|
|
477
|
+
return await callApi(url, {
|
|
478
|
+
method: 'POST',
|
|
479
|
+
headers: { 'Content-Type': 'application/json' },
|
|
480
|
+
body: JSON.stringify({
|
|
481
|
+
email,
|
|
482
|
+
serial,
|
|
483
|
+
hardware_fingerprint: hardwareFingerprint,
|
|
484
|
+
hardware_fingerprint_version: exports.HARDWARE_FINGERPRINT_VERSION,
|
|
485
|
+
signature_seed_version: exports.SIGNATURE_SEED_VERSION,
|
|
486
|
+
}),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Activate a hardware-bound trial for the product.
|
|
491
|
+
*
|
|
492
|
+
* @returns The signed Product Hub trial response.
|
|
493
|
+
* @throws {@link ProductHubError} If fingerprint generation, networking, response parsing,
|
|
494
|
+
* or the trial API request fails.
|
|
495
|
+
* @remarks This low-level method returns API data without verifying or persisting it. Use
|
|
496
|
+
* {@link AudioPluginClient.activateTrial} for the complete plugin workflow.
|
|
497
|
+
* @example
|
|
498
|
+
* ```typescript
|
|
499
|
+
* const trial = await apiClient.activateTrial();
|
|
500
|
+
* console.log(trial.trial_days);
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
async activateTrial() {
|
|
504
|
+
const url = `${BASE_URL}/clients/any/products/${this.productId}/trials`;
|
|
505
|
+
return await callApi(url, {
|
|
506
|
+
method: 'POST',
|
|
507
|
+
headers: { 'Content-Type': 'application/json' },
|
|
508
|
+
body: JSON.stringify({
|
|
509
|
+
hardware_fingerprint: this.hwfpUtil.getFingerprint(),
|
|
510
|
+
hardware_fingerprint_version: exports.HARDWARE_FINGERPRINT_VERSION,
|
|
511
|
+
signature_seed_version: exports.SIGNATURE_SEED_VERSION,
|
|
512
|
+
}),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Get the latest product version from the Release distribution channel.
|
|
517
|
+
*
|
|
518
|
+
* @returns The available builds and optional update referral URL for this product.
|
|
519
|
+
* @throws {@link ProductHubError} If the request fails or its response cannot be parsed.
|
|
520
|
+
* @example
|
|
521
|
+
* ```typescript
|
|
522
|
+
* const latest = await apiClient.getLatestProductVersion();
|
|
523
|
+
* console.log(latest.builds);
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
async getLatestProductVersion() {
|
|
527
|
+
const url = `${BASE_URL}/clients/any/products/${this.productId}/latest`;
|
|
528
|
+
return await callApi(url, {
|
|
529
|
+
method: 'GET',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Get downloadable files for a specific product build.
|
|
534
|
+
*
|
|
535
|
+
* @param buildId - Product Hub build identifier.
|
|
536
|
+
* @returns The files attached to the selected build.
|
|
537
|
+
* @throws {@link ProductHubError} If the request fails or its response cannot be parsed.
|
|
538
|
+
* @example
|
|
539
|
+
* ```typescript
|
|
540
|
+
* const files = await apiClient.getBuildFiles('build-id');
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
async getBuildFiles(buildId) {
|
|
544
|
+
const url = `${BASE_URL}/clients/any/products/${this.productId}/builds/${buildId}/files`;
|
|
545
|
+
return await callApi(url, {
|
|
546
|
+
method: 'GET',
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
ProductHub.APIClient = APIClient;
|
|
551
|
+
/**
|
|
552
|
+
* Complete audio plugin licensing client with automatic activation management.
|
|
553
|
+
*
|
|
554
|
+
* This is the main class for integrating Gorilla Engine Product Hub licensing
|
|
555
|
+
* into your audio plugins. It handles everything from user authentication to
|
|
556
|
+
* local file persistence, signature verification, and trial management.
|
|
557
|
+
*
|
|
558
|
+
* ## Key Features
|
|
559
|
+
* - 🎫 **License Activation**: Activate full licenses with user credentials
|
|
560
|
+
* - ⏰ **Trial Management**: Start trials and track remaining time
|
|
561
|
+
* - 💾 **Local Persistence**: Automatic .ops/.lts file management
|
|
562
|
+
* - 🔄 **Subscription Auto-Refresh**: Silently refreshes persisted subscription activations when cached tokens are available
|
|
563
|
+
* - 🔒 **Signature Verification**: Built-in RSA-SHA256 cryptographic verification
|
|
564
|
+
* - 🖥️ **Hardware Fingerprinting**: Automatic system identification when used with Gorilla Engine "Hardware fingerprinting" native module
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```typescript
|
|
568
|
+
* import { ProductHub, ActivationStatus, TrialExpiredError, ProductHubError, NotAuthorizedError } from "@gorilla-engine-sdk/ge-product-hub";
|
|
569
|
+
*
|
|
570
|
+
* const client = new ProductHub.AudioPluginClient({
|
|
571
|
+
* manufacturerId: "7222d546-9c72-43d5-8bd4-1667ce390ea4",
|
|
572
|
+
* productId: "1487a36d-9bc8-4220-ba31-9998d743071f",
|
|
573
|
+
* productName: "Groove Master 3000",
|
|
574
|
+
* productPublicKey: `-----BEGIN PUBLIC KEY-----
|
|
575
|
+
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
|
576
|
+
* -----END PUBLIC KEY-----`,
|
|
577
|
+
* userResourceBaseDirectory: "/Users/<username>/Library/Application Support/<manufacturername>"
|
|
578
|
+
* });
|
|
579
|
+
*
|
|
580
|
+
* ///////////////////////////////////////////
|
|
581
|
+
* // check whether we can unlock the plugin or need to show the activation prompt
|
|
582
|
+
* if( client.isActivated() ) {
|
|
583
|
+
* // already activated - ready to go!
|
|
584
|
+
* console.log("Plugin ready to use!");
|
|
585
|
+
*
|
|
586
|
+
* // unlock via function defined below
|
|
587
|
+
* unlockPlugin();
|
|
588
|
+
*
|
|
589
|
+
* if( client.getActivationStatus() == ActivationStatus.Trial ) {
|
|
590
|
+
* // if desired: disable non-realtime bouncing when in trial mode
|
|
591
|
+
* GorillaEngine.getPluginNRTB(false);
|
|
592
|
+
* }
|
|
593
|
+
* } else {
|
|
594
|
+
* // not yet activated, neither with a trial nor regular activation
|
|
595
|
+
* showActivationOverlay();
|
|
596
|
+
* }
|
|
597
|
+
*
|
|
598
|
+
*
|
|
599
|
+
*
|
|
600
|
+
* ///////////////////////////////////////////
|
|
601
|
+
* // Handle trial activation
|
|
602
|
+
* try {
|
|
603
|
+
* const success = await client.activateTrial();
|
|
604
|
+
* if( success ) {
|
|
605
|
+
* unlockPlugin();
|
|
606
|
+
* hideActivationOverlay();
|
|
607
|
+
* showMessageBox(`Trial active. ${getRemainingTrialDays(client.remainingTrialTime())} left.`);
|
|
608
|
+
* }
|
|
609
|
+
* } catch( ex ) {
|
|
610
|
+
* if( ex instanceof TrialExpiredError ) {
|
|
611
|
+
* activationOverlay.setMessage("Trial expired. Please activate with license.");
|
|
612
|
+
* } else {
|
|
613
|
+
* activationOverlay.setMessage(`An error occurred: ${(ex as ProductHubError).message}`);
|
|
614
|
+
* }
|
|
615
|
+
* }
|
|
616
|
+
*
|
|
617
|
+
*
|
|
618
|
+
* ///////////////////////////////////////////
|
|
619
|
+
* // Handle activation with license
|
|
620
|
+
* try {
|
|
621
|
+
* const success = await client.activateProduct({
|
|
622
|
+
* email: "user@example.com",
|
|
623
|
+
* password: "secret"
|
|
624
|
+
* });
|
|
625
|
+
*
|
|
626
|
+
* if( success ) {
|
|
627
|
+
* unlockPlugin();
|
|
628
|
+
* hideActivationOverlay();
|
|
629
|
+
* aboutDialog.setSerialCodeForDisplay(client.licenseSerial());
|
|
630
|
+
* }
|
|
631
|
+
* }
|
|
632
|
+
* catch( ex ) {
|
|
633
|
+
* if( ex instanceof NotAuthorizedError ) {
|
|
634
|
+
* activationOverlay.setMessage("Invalid credentials.");
|
|
635
|
+
* } else {
|
|
636
|
+
* activationOverlay.setMessage(`An error occurred: ${(ex as ProductHubError).message}`);
|
|
637
|
+
* }
|
|
638
|
+
* }
|
|
639
|
+
*
|
|
640
|
+
*
|
|
641
|
+
* ///////////////////////////////////////////
|
|
642
|
+
* // assess the activation status of the local installation
|
|
643
|
+
* localActivationState = client.getActivationStatus();
|
|
644
|
+
* switch( localActivationState ) {
|
|
645
|
+
* case ActivationStatus.None:
|
|
646
|
+
* console.log("No successful activation found");
|
|
647
|
+
* break;
|
|
648
|
+
* case ActivationStatus.Trial:
|
|
649
|
+
* console.log("Active trial");
|
|
650
|
+
* break;
|
|
651
|
+
* case ActivationStatus.TrialExpired:
|
|
652
|
+
* console.log("Found a trial activation but the trial period is expired");
|
|
653
|
+
* break;
|
|
654
|
+
* case ActivationStatus.Licensed:
|
|
655
|
+
* console.log(`Plugin is licensed with serial ${client.licenseSerial()}`);
|
|
656
|
+
* const expires = client.licenseExpiration();
|
|
657
|
+
* if (expires) {
|
|
658
|
+
* console.log(`License expires on ${expires.toDateString()}`);
|
|
659
|
+
* }
|
|
660
|
+
* break;
|
|
661
|
+
* case ActivationStatus.SubscriptionExpired:
|
|
662
|
+
* console.log("Subscription license has expired - please renew");
|
|
663
|
+
* break;
|
|
664
|
+
* case ActivationStatus.LicenseExpired:
|
|
665
|
+
* console.log("License has expired - please renew or reactivate");
|
|
666
|
+
* break;
|
|
667
|
+
* default:
|
|
668
|
+
* console.log(`Invalid activation state: ${localActivationState}`)
|
|
669
|
+
* }
|
|
670
|
+
*
|
|
671
|
+
*
|
|
672
|
+
* ///////////////////////////////////////////
|
|
673
|
+
* // unlock audio & MIDI in your plugin
|
|
674
|
+
* function unlockPlugin(trial: boolean = false) {
|
|
675
|
+
* // Note: It is vital to uncheck the following two options in Gorilla Compiler's
|
|
676
|
+
* // "Copy Protection" tab when building your plugin:
|
|
677
|
+
* // 1. MIDI enabled on startup
|
|
678
|
+
* // 2. Audio enabled on startup
|
|
679
|
+
* //
|
|
680
|
+
* // It is better to do it this way round rather than enabling MIDI and audio in Compiler
|
|
681
|
+
* // and disabling it here when isActivated() returns false as the plugin stays unlocked
|
|
682
|
+
* // even if an exception is provoked inside its JavaScript code
|
|
683
|
+
*
|
|
684
|
+
* // Enable MIDI processing
|
|
685
|
+
* GorillaEngine.getPluginMM(true);
|
|
686
|
+
*
|
|
687
|
+
* // Enable audio processing
|
|
688
|
+
* GorillaEngine.getPluginAE(true);
|
|
689
|
+
* }
|
|
690
|
+
*
|
|
691
|
+
*
|
|
692
|
+
* ///////////////////////////////////////////
|
|
693
|
+
* // get remaining trial time in days
|
|
694
|
+
* function getRemainingTrialDays(remainingMs: number) {
|
|
695
|
+
* return Math.floor(remainingMs / (24 * 60 * 60 * 1000));
|
|
696
|
+
* }
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
class AudioPluginClient {
|
|
700
|
+
/**
|
|
701
|
+
* Create a new AudioPluginClient instance.
|
|
702
|
+
*
|
|
703
|
+
* Sets up everything needed for audio plugin licensing including hardware
|
|
704
|
+
* fingerprinting, API communication, and local file management.
|
|
705
|
+
*
|
|
706
|
+
* @remarks
|
|
707
|
+
* The constructor performs two fire-and-forget background checks:
|
|
708
|
+
* - automatic product update check (`checkForUpdates()`)
|
|
709
|
+
* - automatic silent subscription refresh (`checkUpdateSubscription()`) when
|
|
710
|
+
* both a local subscription activation and a stored login token exist
|
|
711
|
+
*
|
|
712
|
+
* These checks are not awaited by construction and may log asynchronous failures.
|
|
713
|
+
*
|
|
714
|
+
* @param options - Configuration options for the client
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* ```typescript
|
|
718
|
+
* const client = new ProductHub.AudioPluginClient({
|
|
719
|
+
* manufacturerId: "7222d546-9c72-43d5-8bd4-1667ce390ea4",
|
|
720
|
+
* productId: "1487a36d-9bc8-4220-ba31-9998d743071f",
|
|
721
|
+
* productName: "Groove Master 3000",
|
|
722
|
+
* productPublicKey: `-----BEGIN PUBLIC KEY-----
|
|
723
|
+
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
|
724
|
+
* -----END PUBLIC KEY-----`,
|
|
725
|
+
* userResourceBaseDirectory: "/Users/<username>/Library/Application Support/<manufacturername>"
|
|
726
|
+
* });
|
|
727
|
+
* ```
|
|
728
|
+
*
|
|
729
|
+
* @example Testing with fingerprint override
|
|
730
|
+
* ```typescript
|
|
731
|
+
* // For automated testing - never use in production!
|
|
732
|
+
* const testClient = new ProductHub.AudioPluginClient({
|
|
733
|
+
* // ... other options
|
|
734
|
+
* hardwareFingerprint: "test-fingerprint-12345"
|
|
735
|
+
* });
|
|
736
|
+
* ```
|
|
737
|
+
*
|
|
738
|
+
* @throws {@link ProductHubError} If required parameters are missing
|
|
739
|
+
*/
|
|
740
|
+
constructor(options) {
|
|
741
|
+
this.updateCheckResult = null;
|
|
742
|
+
const { manufacturerId, productId, productName, productPublicKey, userResourceBaseDirectory, hardwareFingerprint, currentVersion, currentBuildNumber, } = options;
|
|
743
|
+
if (!manufacturerId) {
|
|
744
|
+
throw new errors_1.ProductHubError('manufacturerId is required when constructing AudioPluginClient');
|
|
745
|
+
}
|
|
746
|
+
if (!productId) {
|
|
747
|
+
throw new errors_1.ProductHubError('productId is required when constructing AudioPluginClient');
|
|
748
|
+
}
|
|
749
|
+
if (!productName) {
|
|
750
|
+
throw new errors_1.ProductHubError('productName is required when constructing AudioPluginClient');
|
|
751
|
+
}
|
|
752
|
+
if (!productPublicKey) {
|
|
753
|
+
throw new errors_1.ProductHubError('productPublicKey is required when constructing AudioPluginClient');
|
|
754
|
+
}
|
|
755
|
+
if (hasLeadingWhitespaceLines(productPublicKey)) {
|
|
756
|
+
throw new errors_1.ProductHubError('productPublicKey must not contain lines with leading whitespaces');
|
|
757
|
+
}
|
|
758
|
+
if (!userResourceBaseDirectory) {
|
|
759
|
+
throw new errors_1.ProductHubError('userResourceBaseDirectory is required when constructing AudioPluginClient');
|
|
760
|
+
}
|
|
761
|
+
this.apiClient = new APIClient({
|
|
762
|
+
manufacturerId,
|
|
763
|
+
productId,
|
|
764
|
+
});
|
|
765
|
+
this.manufacturerId = manufacturerId;
|
|
766
|
+
this.productId = productId;
|
|
767
|
+
this.productName = productName;
|
|
768
|
+
this.productPublicKey = productPublicKey;
|
|
769
|
+
this.userResourceBaseDirectory = userResourceBaseDirectory;
|
|
770
|
+
this.currentVersion = currentVersion ?? null;
|
|
771
|
+
this.currentBuildNumber = currentBuildNumber ?? null;
|
|
772
|
+
this.hwfpUtil = HardwareFingerprintUtil.getInstance();
|
|
773
|
+
this.isTestEnvironment = isTestEnvironment();
|
|
774
|
+
if (hardwareFingerprint) {
|
|
775
|
+
if (!this.isTestEnvironment) {
|
|
776
|
+
console.warn('Using overridden hardware fingerprint - not intended for production use!');
|
|
777
|
+
}
|
|
778
|
+
// hwfp util is a singleton so APIClient will also use the same override
|
|
779
|
+
this.hwfpUtil.setFingerprintOverride(hardwareFingerprint);
|
|
780
|
+
}
|
|
781
|
+
// Automatically check for updates on construction (fire-and-forget)
|
|
782
|
+
this.checkForUpdates().catch((ex) => {
|
|
783
|
+
console.error('Update check failed during AudioPluginClient construction:', ex);
|
|
784
|
+
});
|
|
785
|
+
this.checkUpdateSubscription().catch((ex) => {
|
|
786
|
+
if (!this.isTestEnvironment) {
|
|
787
|
+
console.error('Subscription update check failed during AudioPluginClient construction:', ex);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Check and silently refresh persisted subscription activations when possible.
|
|
793
|
+
*
|
|
794
|
+
* If a local activation exists and is a subscription, and a login token is available
|
|
795
|
+
* in the configured credential backend, this method calls {@link updateSubscription}.
|
|
796
|
+
*
|
|
797
|
+
* @returns Promise that resolves to `true` if a newer subscription activation was persisted
|
|
798
|
+
* @remarks Returns `false` unless a local subscription and usable stored session both exist.
|
|
799
|
+
* A successful refresh may overwrite the local activation with a later expiration.
|
|
800
|
+
* @example
|
|
801
|
+
* ```typescript
|
|
802
|
+
* const refreshed = await client.checkUpdateSubscription();
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
async checkUpdateSubscription() {
|
|
806
|
+
const localActivation = this.loadActivationFromDisk();
|
|
807
|
+
if (!localActivation || localActivation.type !== types_1.LicenseType.Subscription) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
if (!(await this.apiClient.hasStoredSession())) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
return await this.updateSubscription();
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Refresh an existing persisted subscription activation from the Product Hub API.
|
|
817
|
+
*
|
|
818
|
+
* This method uses the token persisted in the configured credential backend to call product
|
|
819
|
+
* activation without interactive credentials. If the retrieved subscription has a later
|
|
820
|
+
* expiration timestamp than the currently persisted subscription activation, the local
|
|
821
|
+
* activation file is overwritten.
|
|
822
|
+
*
|
|
823
|
+
* @returns Promise that resolves to `true` if local subscription activation was updated
|
|
824
|
+
* @remarks Only an existing subscription can be updated, and only when the verified remote
|
|
825
|
+
* activation expires later. Non-applicable cases and operational failures return `false`.
|
|
826
|
+
* @example
|
|
827
|
+
* ```typescript
|
|
828
|
+
* const updated = await client.updateSubscription();
|
|
829
|
+
* ```
|
|
830
|
+
*/
|
|
831
|
+
async updateSubscription() {
|
|
832
|
+
try {
|
|
833
|
+
const existingActivation = this.loadActivationFromDisk();
|
|
834
|
+
if (!existingActivation || existingActivation.type !== types_1.LicenseType.Subscription) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
const existingExpiration = existingActivation.expires
|
|
838
|
+
? new Date(existingActivation.expires).getTime()
|
|
839
|
+
: null;
|
|
840
|
+
if (!existingExpiration) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
const result = await this.apiClient.activateProductWithStoredSession();
|
|
844
|
+
const refreshedActivation = result.activation;
|
|
845
|
+
if (result.sessionStatus !== types_1.SessionStatus.Valid ||
|
|
846
|
+
!refreshedActivation ||
|
|
847
|
+
refreshedActivation.type !== types_1.LicenseType.Subscription) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
if (!this.verifyActivation(refreshedActivation)) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
const refreshedExpiration = refreshedActivation.expires
|
|
854
|
+
? new Date(refreshedActivation.expires).getTime()
|
|
855
|
+
: null;
|
|
856
|
+
if (!refreshedExpiration || refreshedExpiration <= existingExpiration) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
this.persistActivationOnDisk(refreshedActivation);
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Activate your audio plugin with a full license using user credentials.
|
|
868
|
+
*
|
|
869
|
+
* This method authenticates the user, activates the product, verifies the activation
|
|
870
|
+
* signature, and automatically persists the activation data to local .ops files
|
|
871
|
+
* for future offline validation.
|
|
872
|
+
*
|
|
873
|
+
* @param params - User credentials for activation
|
|
874
|
+
* @returns Promise that resolves to `true` if activation was successful, `false` for network/server errors
|
|
875
|
+
* @throws {NotAuthorizedError} When email/password credentials are invalid
|
|
876
|
+
*
|
|
877
|
+
* @example
|
|
878
|
+
* ```typescript
|
|
879
|
+
* try {
|
|
880
|
+
* const success = await client.activateProduct({
|
|
881
|
+
* email: "user@example.com",
|
|
882
|
+
* password: "userPassword123"
|
|
883
|
+
* });
|
|
884
|
+
*
|
|
885
|
+
* if (success) {
|
|
886
|
+
* console.log("License activated successfully!");
|
|
887
|
+
* console.log("Serial:", client.licenseSerial());
|
|
888
|
+
* } else {
|
|
889
|
+
* console.log("Activation failed, please retry");
|
|
890
|
+
* }
|
|
891
|
+
* } catch (error) {
|
|
892
|
+
* if (error instanceof NotAuthorizedError) {
|
|
893
|
+
* console.log("Invalid email or password");
|
|
894
|
+
* }
|
|
895
|
+
* }
|
|
896
|
+
* ```
|
|
897
|
+
*
|
|
898
|
+
* @remarks
|
|
899
|
+
* - Hardware fingerprint is detected automatically
|
|
900
|
+
* - Activation data is cryptographically verified before persistence
|
|
901
|
+
* - Safe to call multiple times - won't create duplicate activations
|
|
902
|
+
* - Authentication errors (wrong credentials) throw `NotAuthorizedError`
|
|
903
|
+
* - Network/server errors, missing licenses etc. return `false`
|
|
904
|
+
*/
|
|
905
|
+
async activateProduct(params) {
|
|
906
|
+
try {
|
|
907
|
+
const response = await this.apiClient.activateProduct({
|
|
908
|
+
email: params.email,
|
|
909
|
+
password: params.password,
|
|
910
|
+
});
|
|
911
|
+
if (this.verifyActivation(response)) {
|
|
912
|
+
this.persistActivationOnDisk(response);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
catch (ex) {
|
|
918
|
+
if (ex instanceof errors_1.NotAuthorizedError) {
|
|
919
|
+
throw ex;
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
if (!this.isTestEnvironment) {
|
|
923
|
+
console.error(`Activation failed: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
924
|
+
}
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Activate the product using only email and serial — no password or login required.
|
|
931
|
+
*
|
|
932
|
+
* The license is claimed and activated in a single call. Use this flow when the
|
|
933
|
+
* manufacturer has "Authentication Type = None" and "Email+Serial Claiming" enabled
|
|
934
|
+
* in Product Hub (i.e. no SSO provider is configured).
|
|
935
|
+
*
|
|
936
|
+
* @param params - Email and serial received from purchase
|
|
937
|
+
* @returns Promise that resolves to `true` if activation was successful, `false` otherwise
|
|
938
|
+
* @throws {NotAuthorizedError} When email+serial claiming is not enabled for this manufacturer, or the serial is not found
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* ```typescript
|
|
942
|
+
* try {
|
|
943
|
+
* const success = await client.activateProductWithSerial({
|
|
944
|
+
* email: 'user@example.com',
|
|
945
|
+
* serial: 'STD-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX',
|
|
946
|
+
* });
|
|
947
|
+
* if (success) {
|
|
948
|
+
* unlockPlugin();
|
|
949
|
+
* aboutDialog.setSerialCodeForDisplay(client.licenseSerial());
|
|
950
|
+
* }
|
|
951
|
+
* } catch (ex) {
|
|
952
|
+
* if (ex instanceof NotAuthorizedError) {
|
|
953
|
+
* activationOverlay.setMessage('Unable to activate. Check your email and serial code.');
|
|
954
|
+
* }
|
|
955
|
+
* }
|
|
956
|
+
* ```
|
|
957
|
+
*/
|
|
958
|
+
async activateProductWithSerial(params) {
|
|
959
|
+
try {
|
|
960
|
+
const response = await this.apiClient.activateProductWithEmailAndSerial(params.email, params.serial);
|
|
961
|
+
if (this.verifyActivation(response)) {
|
|
962
|
+
this.persistActivationOnDisk(response);
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
catch (ex) {
|
|
968
|
+
if (ex instanceof errors_1.NotAuthorizedError) {
|
|
969
|
+
throw ex;
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
if (!this.isTestEnvironment) {
|
|
973
|
+
console.error(`Activation failed: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
974
|
+
}
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Claim a reseller-purchased serial to the user's account and activate in one call.
|
|
981
|
+
*
|
|
982
|
+
* Use this when a user already has an account (SSO login) but bought a license
|
|
983
|
+
* serial from a reseller shop. The serial is claimed to their account and then
|
|
984
|
+
* activated, all in a single operation.
|
|
985
|
+
*
|
|
986
|
+
* This method always logs in with the supplied credentials. To claim a serial using
|
|
987
|
+
* the active stored session, call {@link claimAndActivateLicenseWithStoredSession}.
|
|
988
|
+
*
|
|
989
|
+
* @param params - User credentials and reseller serial
|
|
990
|
+
* @returns Promise that resolves to `true` if activation was successful, `false` otherwise
|
|
991
|
+
* @throws {@link NotAuthorizedError} When the supplied credentials are invalid.
|
|
992
|
+
*
|
|
993
|
+
* @example
|
|
994
|
+
* ```typescript
|
|
995
|
+
* try {
|
|
996
|
+
* const success = await client.claimAndActivateLicenseWithSerial({
|
|
997
|
+
* email: 'user@example.com',
|
|
998
|
+
* password: 'secret',
|
|
999
|
+
* serial: 'STD-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX',
|
|
1000
|
+
* });
|
|
1001
|
+
* if (success) {
|
|
1002
|
+
* unlockPlugin();
|
|
1003
|
+
* aboutDialog.setSerialCodeForDisplay(client.licenseSerial());
|
|
1004
|
+
* }
|
|
1005
|
+
* } catch (ex) {
|
|
1006
|
+
* if (ex instanceof NotAuthorizedError) {
|
|
1007
|
+
* activationOverlay.setMessage('Invalid credentials or serial already owned by another account.');
|
|
1008
|
+
* }
|
|
1009
|
+
* }
|
|
1010
|
+
* ```
|
|
1011
|
+
*
|
|
1012
|
+
* @remarks A verified activation is persisted locally. Missing, disabled, or conflicting
|
|
1013
|
+
* serials and other non-authentication failures return `false`.
|
|
1014
|
+
*/
|
|
1015
|
+
async claimAndActivateLicenseWithSerial(params) {
|
|
1016
|
+
try {
|
|
1017
|
+
const response = await this.apiClient.activateProduct({
|
|
1018
|
+
email: params.email,
|
|
1019
|
+
password: params.password,
|
|
1020
|
+
serial: params.serial,
|
|
1021
|
+
});
|
|
1022
|
+
if (this.verifyActivation(response)) {
|
|
1023
|
+
this.persistActivationOnDisk(response);
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
catch (ex) {
|
|
1029
|
+
if (ex instanceof errors_1.NotAuthorizedError) {
|
|
1030
|
+
throw ex;
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
if (!this.isTestEnvironment) {
|
|
1034
|
+
console.error(`Activation failed: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1035
|
+
}
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Claim a reseller serial and activate it using the active stored session.
|
|
1042
|
+
*
|
|
1043
|
+
* @param params - Reseller serial to claim and activate.
|
|
1044
|
+
* @returns `true` when a signed activation is verified and persisted; otherwise `false`.
|
|
1045
|
+
* @remarks
|
|
1046
|
+
* This method never performs a credential login. Missing or invalid sessions, serial
|
|
1047
|
+
* conflicts, network failures, and invalid activation signatures are converted to `false`.
|
|
1048
|
+
* Use {@link restoreActivationFromStoredSession} when the caller needs independent session
|
|
1049
|
+
* and activation statuses.
|
|
1050
|
+
* @example
|
|
1051
|
+
* ```typescript
|
|
1052
|
+
* const activated = await client.claimAndActivateLicenseWithStoredSession({
|
|
1053
|
+
* serial: 'STD-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX',
|
|
1054
|
+
* });
|
|
1055
|
+
* ```
|
|
1056
|
+
*/
|
|
1057
|
+
async claimAndActivateLicenseWithStoredSession(params) {
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await this.apiClient.activateProductWithStoredSession(params.serial);
|
|
1060
|
+
if (result.activation && this.verifyActivation(result.activation)) {
|
|
1061
|
+
this.persistActivationOnDisk(result.activation);
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
catch (ex) {
|
|
1067
|
+
if (!this.isTestEnvironment) {
|
|
1068
|
+
console.error(`Activation failed: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1069
|
+
}
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Check whether a usable login session is stored for this product and device.
|
|
1075
|
+
*
|
|
1076
|
+
* @returns A promise resolving to `true` when a usable session exists; otherwise `false`.
|
|
1077
|
+
* @remarks Invalid stored tokens are removed by the underlying credential-store check.
|
|
1078
|
+
*/
|
|
1079
|
+
async hasStoredSession() {
|
|
1080
|
+
return await this.apiClient.hasStoredSession();
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Read non-sensitive claims from the active stored login session.
|
|
1084
|
+
*
|
|
1085
|
+
* @returns The stored session's email, user ID, and expiration, or `null` when no usable
|
|
1086
|
+
* session exists. The bearer token is never exposed.
|
|
1087
|
+
* @example
|
|
1088
|
+
* ```typescript
|
|
1089
|
+
* const session = await client.getStoredSessionInfo();
|
|
1090
|
+
* if (session) console.log(session.email, session.expiresAt);
|
|
1091
|
+
* ```
|
|
1092
|
+
*/
|
|
1093
|
+
async getStoredSessionInfo() {
|
|
1094
|
+
return await this.apiClient.getStoredSessionInfo();
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Clear the active stored login session.
|
|
1098
|
+
*
|
|
1099
|
+
* @returns A promise that resolves after the in-memory token and persisted credential have
|
|
1100
|
+
* been cleared on a best-effort basis.
|
|
1101
|
+
* @remarks This does not remove the locally persisted `.ops` activation file.
|
|
1102
|
+
* @example
|
|
1103
|
+
* ```typescript
|
|
1104
|
+
* await client.clearStoredSession();
|
|
1105
|
+
* ```
|
|
1106
|
+
*/
|
|
1107
|
+
async clearStoredSession() {
|
|
1108
|
+
await this.apiClient.clearStoredSession();
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Restore product activation using the active stored session.
|
|
1112
|
+
*
|
|
1113
|
+
* @returns Independent session validity and local product activation statuses.
|
|
1114
|
+
* @throws {@link ProductHubError} If the API request fails unexpectedly or the returned
|
|
1115
|
+
* activation has an invalid signature.
|
|
1116
|
+
* @remarks
|
|
1117
|
+
* Session validity and product activation are independent: a valid session can return an
|
|
1118
|
+
* unlicensed or expired activation status. Valid signed activations are persisted locally;
|
|
1119
|
+
* invalid sessions are removed by the underlying API client.
|
|
1120
|
+
* @example
|
|
1121
|
+
* ```typescript
|
|
1122
|
+
* const result = await client.restoreActivationFromStoredSession();
|
|
1123
|
+
* if (
|
|
1124
|
+
* result.activationStatus === ActivationStatus.Licensed ||
|
|
1125
|
+
* result.activationStatus === ActivationStatus.Subscription
|
|
1126
|
+
* ) {
|
|
1127
|
+
* unlockPlugin();
|
|
1128
|
+
* }
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
async restoreActivationFromStoredSession() {
|
|
1132
|
+
const result = await this.apiClient.activateProductWithStoredSession();
|
|
1133
|
+
if (result.sessionStatus !== types_1.SessionStatus.Valid || !result.activation) {
|
|
1134
|
+
return {
|
|
1135
|
+
sessionStatus: result.sessionStatus,
|
|
1136
|
+
activationStatus: this.getActivationStatus(),
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
if (!this.verifyActivation(result.activation)) {
|
|
1140
|
+
throw new errors_1.ProductHubError('Stored-session activation signature verification failed');
|
|
1141
|
+
}
|
|
1142
|
+
this.persistActivationOnDisk(result.activation);
|
|
1143
|
+
return {
|
|
1144
|
+
sessionStatus: types_1.SessionStatus.Valid,
|
|
1145
|
+
activationStatus: this.getActivationStatus(),
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Activate a trial period for your audio plugin.
|
|
1150
|
+
*
|
|
1151
|
+
* Starts a new trial or reuses an existing valid trial activation.
|
|
1152
|
+
* No user authentication is required - trials are tied to the hardware fingerprint.
|
|
1153
|
+
* The trial data is automatically verified and persisted to local .ops files.
|
|
1154
|
+
*
|
|
1155
|
+
* @returns Promise that resolves to `true` if trial activation was successful, `false` otherwise
|
|
1156
|
+
* @throws {TrialExpiredError} When a trial was already used on this machine and has expired
|
|
1157
|
+
*
|
|
1158
|
+
* @example
|
|
1159
|
+
* ```typescript
|
|
1160
|
+
* const success = await client.activateTrial();
|
|
1161
|
+
*
|
|
1162
|
+
* if (success) {
|
|
1163
|
+
* const remainingMs = client.remainingTrialTime();
|
|
1164
|
+
* const days = Math.floor(remainingMs / (24 * 60 * 60 * 1000));
|
|
1165
|
+
* console.log(`Trial activated! ${days} days remaining.`);
|
|
1166
|
+
* } else {
|
|
1167
|
+
* console.log("Trial activation failed");
|
|
1168
|
+
* }
|
|
1169
|
+
* ```
|
|
1170
|
+
*
|
|
1171
|
+
* @remarks
|
|
1172
|
+
* - Hardware fingerprint prevents trial abuse across different machines
|
|
1173
|
+
* - Trial period is determined by Product Hub configuration
|
|
1174
|
+
* - Automatically updates last launch time
|
|
1175
|
+
* - Expired trials throw `TrialExpiredError`
|
|
1176
|
+
* - Safe to call multiple times - reuses existing valid trials
|
|
1177
|
+
*/
|
|
1178
|
+
async activateTrial() {
|
|
1179
|
+
try {
|
|
1180
|
+
const response = await this.apiClient.activateTrial();
|
|
1181
|
+
if (this.verifyTrial(response)) {
|
|
1182
|
+
this.persistTrialActivationOnDisk(response);
|
|
1183
|
+
return this.remainingTrialTime() > 0;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
catch (ex) {
|
|
1187
|
+
if (!this.isTestEnvironment) {
|
|
1188
|
+
console.error(`Trial activation failed: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (ex.status === 403 &&
|
|
1191
|
+
ex.message === 'Trial expired') {
|
|
1192
|
+
throw new errors_1.TrialExpiredError();
|
|
1193
|
+
}
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Check if the plugin is activated with a full license.
|
|
1200
|
+
*
|
|
1201
|
+
* Verifies that a valid license activation exists locally, including
|
|
1202
|
+
* signature verification and hardware fingerprint validation.
|
|
1203
|
+
*
|
|
1204
|
+
* @returns `true` if the plugin has a valid full license, `false` otherwise
|
|
1205
|
+
*
|
|
1206
|
+
* @example
|
|
1207
|
+
* ```typescript
|
|
1208
|
+
* if (client.isActivatedWithLicense()) {
|
|
1209
|
+
* console.log("Full license active!");
|
|
1210
|
+
* console.log("License serial:", client.licenseSerial());
|
|
1211
|
+
* } else {
|
|
1212
|
+
* console.log("No license - maybe try activation or trial");
|
|
1213
|
+
* }
|
|
1214
|
+
* ```
|
|
1215
|
+
*
|
|
1216
|
+
* @remarks
|
|
1217
|
+
* - Validates hardware fingerprint to prevent license transfer
|
|
1218
|
+
* - Safe to call frequently - uses cached local data
|
|
1219
|
+
*/
|
|
1220
|
+
isActivatedWithLicense() {
|
|
1221
|
+
try {
|
|
1222
|
+
const activation = this.loadActivationFromDisk();
|
|
1223
|
+
if (!activation) {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
// Check if license has expired
|
|
1227
|
+
if (activation.expires) {
|
|
1228
|
+
const expirationDate = new Date(activation.expires);
|
|
1229
|
+
const now = new Date();
|
|
1230
|
+
if (now > expirationDate) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return this.verifyActivation(activation);
|
|
1235
|
+
}
|
|
1236
|
+
catch (ex) {
|
|
1237
|
+
if (!this.isTestEnvironment) {
|
|
1238
|
+
console.error(`Could not assess whether product is activated with license: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Get the remaining trial time in milliseconds.
|
|
1245
|
+
*
|
|
1246
|
+
* Calculates how much time is left in the current trial period, including
|
|
1247
|
+
* signature verification and hardware fingerprint validation. Returns 0
|
|
1248
|
+
* if no valid trial exists or if the trial has expired.
|
|
1249
|
+
*
|
|
1250
|
+
* @returns Remaining trial time in milliseconds, or 0 if no valid trial
|
|
1251
|
+
*
|
|
1252
|
+
* @example
|
|
1253
|
+
* ```typescript
|
|
1254
|
+
* const remainingMs = client.remainingTrialTime();
|
|
1255
|
+
*
|
|
1256
|
+
* if (remainingMs > 0) {
|
|
1257
|
+
* const days = Math.floor(remainingMs / (24 * 60 * 60 * 1000));
|
|
1258
|
+
* const hours = Math.floor((remainingMs % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
|
|
1259
|
+
* console.log(`Trial: ${days} days, ${hours} hours remaining`);
|
|
1260
|
+
* } else {
|
|
1261
|
+
* console.log("No active trial");
|
|
1262
|
+
* }
|
|
1263
|
+
* ```
|
|
1264
|
+
*
|
|
1265
|
+
* @remarks
|
|
1266
|
+
* - Automatically updates last launch time when called
|
|
1267
|
+
* - Performs cryptographic verification of trial data
|
|
1268
|
+
* - Returns exact milliseconds for precise timing calculations
|
|
1269
|
+
*/
|
|
1270
|
+
remainingTrialTime() {
|
|
1271
|
+
try {
|
|
1272
|
+
const now = Date.now();
|
|
1273
|
+
const lastLaunch = this.readLastTrialLaunchTime() || 0;
|
|
1274
|
+
if (lastLaunch > now) {
|
|
1275
|
+
// Prevent clock tampering by ensuring last launch time is not in the future
|
|
1276
|
+
if (!this.isTestEnvironment) {
|
|
1277
|
+
console.warn('System clock appears to have been set back - invalidating trial');
|
|
1278
|
+
}
|
|
1279
|
+
return 0;
|
|
1280
|
+
}
|
|
1281
|
+
const trial = this.loadTrialActivationFromDisk();
|
|
1282
|
+
if (!trial) {
|
|
1283
|
+
return 0;
|
|
1284
|
+
}
|
|
1285
|
+
const valid = this.verifyTrial(trial);
|
|
1286
|
+
if (!valid) {
|
|
1287
|
+
return 0;
|
|
1288
|
+
}
|
|
1289
|
+
const periodMs = trial.trial_days * 24 * 60 * 60 * 1000;
|
|
1290
|
+
const remaining = Math.max(0, trial.timestamp + periodMs - now);
|
|
1291
|
+
this.updateLastTrialLaunchTime();
|
|
1292
|
+
return remaining;
|
|
1293
|
+
}
|
|
1294
|
+
catch (ex) {
|
|
1295
|
+
if (!this.isTestEnvironment) {
|
|
1296
|
+
console.error(`Could not assess remaining trial time: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1297
|
+
}
|
|
1298
|
+
return 0;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Check if the plugin is activated and ready to use.
|
|
1303
|
+
*
|
|
1304
|
+
* Returns `true` if the plugin has either a valid full license OR a valid trial
|
|
1305
|
+
* with remaining time. This is the primary method to check if your plugin
|
|
1306
|
+
* should be functional.
|
|
1307
|
+
*
|
|
1308
|
+
* @returns `true` if plugin is activated (licensed or trial), `false` otherwise
|
|
1309
|
+
*
|
|
1310
|
+
* @example
|
|
1311
|
+
* ```typescript
|
|
1312
|
+
* // Basic usage
|
|
1313
|
+
* if (client.isActivated()) {
|
|
1314
|
+
* // Enable all plugin features
|
|
1315
|
+
* enableAudioProcessing();
|
|
1316
|
+
* } else {
|
|
1317
|
+
* // Show activation dialog or disable features
|
|
1318
|
+
* showActivationDialog();
|
|
1319
|
+
* }
|
|
1320
|
+
*
|
|
1321
|
+
* // Advanced usage with status details
|
|
1322
|
+
* if (client.isActivated()) {
|
|
1323
|
+
* if (client.isActivatedWithLicense()) {
|
|
1324
|
+
* console.log("Full license - all features activated");
|
|
1325
|
+
* } else {
|
|
1326
|
+
* const days = Math.floor(client.remainingTrialTime() / (24 * 60 * 60 * 1000));
|
|
1327
|
+
* console.log(`Trial mode - ${days} days remaining`);
|
|
1328
|
+
* }
|
|
1329
|
+
* }
|
|
1330
|
+
* ```
|
|
1331
|
+
*
|
|
1332
|
+
* @remarks
|
|
1333
|
+
* - This is typically the main method you'll use in your plugin logic
|
|
1334
|
+
* - Combines both license and trial validation
|
|
1335
|
+
* - Efficient - uses cached data where possible
|
|
1336
|
+
*/
|
|
1337
|
+
isActivated() {
|
|
1338
|
+
return this.isActivatedWithLicense() || this.remainingTrialTime() > 0;
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Get detailed activation status information.
|
|
1342
|
+
*
|
|
1343
|
+
* Provides more granular information about the current activation state
|
|
1344
|
+
* than the simple boolean methods. Useful for showing specific UI states
|
|
1345
|
+
* or handling different scenarios in your plugin.
|
|
1346
|
+
*
|
|
1347
|
+
* @returns Current activation status as an enum value
|
|
1348
|
+
*
|
|
1349
|
+
* @example
|
|
1350
|
+
* ```typescript
|
|
1351
|
+
* import { ActivationStatus } from "@gorilla-engine-sdk/ge-product-hub";
|
|
1352
|
+
*
|
|
1353
|
+
* switch (client.getActivationStatus()) {
|
|
1354
|
+
* case ActivationStatus.Licensed:
|
|
1355
|
+
* showLicensedUI();
|
|
1356
|
+
* console.log("Serial:", client.licenseSerial());
|
|
1357
|
+
* break;
|
|
1358
|
+
*
|
|
1359
|
+
* case ActivationStatus.Trial:
|
|
1360
|
+
* const days = Math.floor(client.remainingTrialTime() / (24 * 60 * 60 * 1000));
|
|
1361
|
+
* showTrialUI(days);
|
|
1362
|
+
* break;
|
|
1363
|
+
*
|
|
1364
|
+
* case ActivationStatus.TrialExpired:
|
|
1365
|
+
* showTrialExpiredUI();
|
|
1366
|
+
* break;
|
|
1367
|
+
*
|
|
1368
|
+
* case ActivationStatus.None:
|
|
1369
|
+
* case ActivationStatus.LicenseExpired:
|
|
1370
|
+
* case ActivationStatus.SubscriptionExpired:
|
|
1371
|
+
* showActivationRequiredUI();
|
|
1372
|
+
* break;
|
|
1373
|
+
* }
|
|
1374
|
+
* ```
|
|
1375
|
+
*
|
|
1376
|
+
* @see {@link ActivationStatus} for all possible status values
|
|
1377
|
+
*
|
|
1378
|
+
* @remarks
|
|
1379
|
+
* - More informative than `isActivated()` or `isActivatedWithLicense()`
|
|
1380
|
+
* - Helps tailor user experience based on exact activation state
|
|
1381
|
+
* - Always returns a valid enum value
|
|
1382
|
+
* - Note that this is the local activation status. This is especially important in regard to trials as there can be a possible edge case.
|
|
1383
|
+
If the user trialed the product in the past and then later deleted the installation and they now want to trial again, the local activation state would be `None` and
|
|
1384
|
+
`activateTrial()` would throw `TrialExpiredError` with the local activation state staying `None`. If on the other hand the installation is not deleted so the activation
|
|
1385
|
+
file still exists, the local activation state would be `TrialExpired`.
|
|
1386
|
+
*/
|
|
1387
|
+
getActivationStatus() {
|
|
1388
|
+
// Check for any persisted license activation on disk (valid or expired)
|
|
1389
|
+
const activation = this.loadActivationFromDisk();
|
|
1390
|
+
if (activation) {
|
|
1391
|
+
// we have a license activation - check if it's valid
|
|
1392
|
+
if (this.isActivatedWithLicense()) {
|
|
1393
|
+
// license is valid
|
|
1394
|
+
if (activation.type === types_1.LicenseType.Subscription) {
|
|
1395
|
+
return types_1.ActivationStatus.Subscription;
|
|
1396
|
+
}
|
|
1397
|
+
return types_1.ActivationStatus.Licensed;
|
|
1398
|
+
}
|
|
1399
|
+
else if (activation.expires) {
|
|
1400
|
+
// License exists but is expired
|
|
1401
|
+
if (activation.type === types_1.LicenseType.Subscription) {
|
|
1402
|
+
return types_1.ActivationStatus.SubscriptionExpired;
|
|
1403
|
+
}
|
|
1404
|
+
return types_1.ActivationStatus.LicenseExpired;
|
|
1405
|
+
}
|
|
1406
|
+
// if we've come to this point, license exists but signature verification failed - treat as None
|
|
1407
|
+
}
|
|
1408
|
+
// No valid license - check for trial activation
|
|
1409
|
+
if (this.remainingTrialTime() > 0) {
|
|
1410
|
+
return types_1.ActivationStatus.Trial;
|
|
1411
|
+
}
|
|
1412
|
+
if (this.loadTrialActivationFromDisk() !== undefined) {
|
|
1413
|
+
return types_1.ActivationStatus.TrialExpired;
|
|
1414
|
+
}
|
|
1415
|
+
return types_1.ActivationStatus.None;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Get the license serial number if the plugin is fully activated.
|
|
1419
|
+
*
|
|
1420
|
+
* Returns the unique serial number associated with the current license
|
|
1421
|
+
* activation. Useful for support purposes, logging, or displaying
|
|
1422
|
+
* license information to users.
|
|
1423
|
+
*
|
|
1424
|
+
* @returns License serial number if licensed, empty string otherwise
|
|
1425
|
+
*
|
|
1426
|
+
* @example
|
|
1427
|
+
* ```typescript
|
|
1428
|
+
* const serial = client.licenseSerial();
|
|
1429
|
+
*
|
|
1430
|
+
* if (serial) {
|
|
1431
|
+
* console.log(`Licensed version - Serial: ${serial}`);
|
|
1432
|
+
* // Maybe show in about dialog
|
|
1433
|
+
* showAboutDialog({ serial });
|
|
1434
|
+
* } else {
|
|
1435
|
+
* console.log("Not licensed");
|
|
1436
|
+
* }
|
|
1437
|
+
* ```
|
|
1438
|
+
*
|
|
1439
|
+
* @remarks
|
|
1440
|
+
* - Only returns a value when `isActivatedWithLicense()` is `true`
|
|
1441
|
+
* - Serial numbers are unique per license activation
|
|
1442
|
+
* - Useful for customer support and license tracking
|
|
1443
|
+
*/
|
|
1444
|
+
licenseSerial() {
|
|
1445
|
+
if (!this.isActivatedWithLicense())
|
|
1446
|
+
return '';
|
|
1447
|
+
const activation = this.loadActivationFromDisk();
|
|
1448
|
+
return activation?.serial || '';
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Get the type of the current license.
|
|
1452
|
+
*
|
|
1453
|
+
* Returns the license type (Standard, NFR, Free, or Subscription) if the
|
|
1454
|
+
* plugin is activated with a valid license. Returns null for trial activations
|
|
1455
|
+
* or when no valid license exists.
|
|
1456
|
+
*
|
|
1457
|
+
* @returns License type if activated with license, null otherwise
|
|
1458
|
+
*
|
|
1459
|
+
* @example
|
|
1460
|
+
* ```typescript
|
|
1461
|
+
* const type = client.licenseType();
|
|
1462
|
+
* if (type) {
|
|
1463
|
+
* console.log(`License type: ${type}`);
|
|
1464
|
+
* if (type === LicenseType.Subscription) {
|
|
1465
|
+
* console.log("Subscription license - check expiration");
|
|
1466
|
+
* }
|
|
1467
|
+
* }
|
|
1468
|
+
* ```
|
|
1469
|
+
*
|
|
1470
|
+
* @remarks
|
|
1471
|
+
* - Returns null for trials (use `getActivationStatus()` to check for trials)
|
|
1472
|
+
* - Useful for displaying license information to users
|
|
1473
|
+
* - Only meaningful when `isActivatedWithLicense()` is `true`
|
|
1474
|
+
*/
|
|
1475
|
+
licenseType() {
|
|
1476
|
+
if (!this.isActivatedWithLicense())
|
|
1477
|
+
return null;
|
|
1478
|
+
const activation = this.loadActivationFromDisk();
|
|
1479
|
+
return activation?.type ? activation.type : null;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Check if the current license is time-limited (subscription or expiring).
|
|
1483
|
+
*
|
|
1484
|
+
* Returns `true` if the persisted license activation has an expiration date.
|
|
1485
|
+
*
|
|
1486
|
+
* @returns `true` if license is time-limited, `false` if perpetual, trial or no license
|
|
1487
|
+
*
|
|
1488
|
+
* @example
|
|
1489
|
+
* ```typescript
|
|
1490
|
+
* if (client.isTimeLimitedLicense()) {
|
|
1491
|
+
* const expires = client.licenseExpiration();
|
|
1492
|
+
* if (expires) {
|
|
1493
|
+
* console.log(`License expires on ${expires.toDateString()}`);
|
|
1494
|
+
* }
|
|
1495
|
+
* } else {
|
|
1496
|
+
* console.log("Perpetual license");
|
|
1497
|
+
* }
|
|
1498
|
+
* ```
|
|
1499
|
+
*
|
|
1500
|
+
* @remarks
|
|
1501
|
+
* - This method does not verify the persisted activation or check whether it has expired
|
|
1502
|
+
* - Helps determine if renewal reminders are needed
|
|
1503
|
+
* - Time-limited licenses have an `expires` date in the activation data
|
|
1504
|
+
*/
|
|
1505
|
+
isTimeLimitedLicense() {
|
|
1506
|
+
const activation = this.loadActivationFromDisk();
|
|
1507
|
+
return activation?.expires ? true : false;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Get the expiration date from the persisted license activation.
|
|
1511
|
+
*
|
|
1512
|
+
* Returns the persisted expiration date, or null if the license is perpetual
|
|
1513
|
+
* (never expires), no license activation exists, or the activation is a trial.
|
|
1514
|
+
* For trials, use `remainingTrialTime()` to check remaining time.
|
|
1515
|
+
*
|
|
1516
|
+
* @returns Expiration date if licensed and has expiration, null otherwise (even for active trials)
|
|
1517
|
+
*
|
|
1518
|
+
* @example
|
|
1519
|
+
* ```typescript
|
|
1520
|
+
* const expires = client.licenseExpiration();
|
|
1521
|
+
* if (expires) {
|
|
1522
|
+
* const daysLeft = Math.ceil((expires.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
1523
|
+
* console.log(`License expires in ${daysLeft} days`);
|
|
1524
|
+
* } else {
|
|
1525
|
+
* console.log("Perpetual license");
|
|
1526
|
+
* }
|
|
1527
|
+
* ```
|
|
1528
|
+
*
|
|
1529
|
+
* @remarks This method does not verify the persisted activation or check whether the date
|
|
1530
|
+
* has passed. Use {@link getActivationStatus} to determine current validity.
|
|
1531
|
+
*/
|
|
1532
|
+
licenseExpiration() {
|
|
1533
|
+
const activation = this.loadActivationFromDisk();
|
|
1534
|
+
return activation?.expires ? new Date(activation.expires) : null;
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Write an offline activation file for support purposes.
|
|
1538
|
+
*
|
|
1539
|
+
* This method creates an offline activation file (`.ops`) containing your product's hardware
|
|
1540
|
+
* fingerprint and configuration details. This is useful when end users don't have internet
|
|
1541
|
+
* access - they can send you this file to obtain their system information, allowing you to
|
|
1542
|
+
* create a manual activation in the Product Hub backend and provide them with a signed
|
|
1543
|
+
* activation file for offline use.
|
|
1544
|
+
*
|
|
1545
|
+
* @param options - Configuration options for the offline activation file
|
|
1546
|
+
* @returns Nothing. Writes `{productName}.ops`, or leaves an existing file untouched when
|
|
1547
|
+
* `overwriteExisting` is `false`.
|
|
1548
|
+
* @throws {@link ProductHubError} If hardware fingerprint generation fails.
|
|
1549
|
+
* @throws {@link Error} If the destination cannot be created or written.
|
|
1550
|
+
*
|
|
1551
|
+
* @example
|
|
1552
|
+
* ```typescript
|
|
1553
|
+
* // Write to default location without overwriting
|
|
1554
|
+
* client.writeOfflineActivationFile();
|
|
1555
|
+
*
|
|
1556
|
+
* // Write to custom location with overwrite enabled
|
|
1557
|
+
* client.writeOfflineActivationFile({
|
|
1558
|
+
* customFolderPath: "/Users/username/Desktop",
|
|
1559
|
+
* overwriteExisting: true
|
|
1560
|
+
* });
|
|
1561
|
+
*
|
|
1562
|
+
* // Just enable overwriting in default location
|
|
1563
|
+
* client.writeOfflineActivationFile({ overwriteExisting: true });
|
|
1564
|
+
* ```
|
|
1565
|
+
*
|
|
1566
|
+
* @remarks
|
|
1567
|
+
* - The file contains hardware fingerprint, product ID, and signature versions
|
|
1568
|
+
* - File is named `{productName}.ops` (e.g., "MyPlugin.ops")
|
|
1569
|
+
* - Safe to call multiple times - respects `overwriteExisting` option
|
|
1570
|
+
* - Automatically creates necessary directories
|
|
1571
|
+
* - For offline activation workflow with customer support
|
|
1572
|
+
*/
|
|
1573
|
+
writeOfflineActivationFile(options = {}) {
|
|
1574
|
+
const { customFolderPath, overwriteExisting = false } = options;
|
|
1575
|
+
const filePath = path.join(path.join(customFolderPath || this.opsFileFolderPath()), `${this.productName}.ops`);
|
|
1576
|
+
if (fs.existsSync(filePath)) {
|
|
1577
|
+
if (!this.isTestEnvironment) {
|
|
1578
|
+
console.log(`Offline .ops file already exists at ${filePath} - ${overwriteExisting ? 'overwriting' : 'not overwriting'}`);
|
|
1579
|
+
}
|
|
1580
|
+
if (overwriteExisting) {
|
|
1581
|
+
fs.unlinkSync(filePath);
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const payload = {
|
|
1588
|
+
signature: 'offline',
|
|
1589
|
+
product_id: this.productId,
|
|
1590
|
+
product_name: this.productName,
|
|
1591
|
+
manufacturer_id: this.manufacturerId,
|
|
1592
|
+
hardware_fingerprint: this.hwfpUtil.getFingerprint(),
|
|
1593
|
+
hardware_fingerprint_version: exports.HARDWARE_FINGERPRINT_VERSION,
|
|
1594
|
+
signature_seed_version: exports.SIGNATURE_SEED_VERSION,
|
|
1595
|
+
};
|
|
1596
|
+
safeWriteJSON(filePath, payload);
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Verify activation signature (v6 format only)
|
|
1600
|
+
*/
|
|
1601
|
+
verifyActivation(input) {
|
|
1602
|
+
const { serial, signature, expires, signature_seed_version } = input;
|
|
1603
|
+
let message;
|
|
1604
|
+
if (signature_seed_version === 5) {
|
|
1605
|
+
// Version 5 signature: fingerprint:serial:product_identifier:version
|
|
1606
|
+
message = `${this.hwfpUtil.getFingerprint()}:${serial}:${this.productName}:${signature_seed_version}`;
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
// Version 6+ signature: fingerprint:serial:product_id:expires:version
|
|
1610
|
+
const expiresTimestamp = expires ? new Date(expires).getTime() : null;
|
|
1611
|
+
message = `${this.hwfpUtil.getFingerprint()}:${serial}:${this.productId}:${expiresTimestamp}:${signature_seed_version || exports.SIGNATURE_SEED_VERSION}`;
|
|
1612
|
+
}
|
|
1613
|
+
return verifySignature(this.productPublicKey, message, signature);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Verify trial signature
|
|
1617
|
+
*/
|
|
1618
|
+
verifyTrial(input) {
|
|
1619
|
+
const { timestamp, trial_days, signature, signature_seed_version } = input;
|
|
1620
|
+
let message;
|
|
1621
|
+
if (signature_seed_version === 5) {
|
|
1622
|
+
// Version 5 trial signature: fingerprint:timestamp:product_identifier:trial_days:version
|
|
1623
|
+
message = `${this.hwfpUtil.getFingerprint()}:${timestamp}:${this.productName}:${trial_days}:${signature_seed_version}`;
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
// Version 6+ trial signature: fingerprint:timestamp:product_id:trial_days:version
|
|
1627
|
+
message = `${this.hwfpUtil.getFingerprint()}:${timestamp}:${this.productId}:${trial_days}:${signature_seed_version || exports.SIGNATURE_SEED_VERSION}`;
|
|
1628
|
+
}
|
|
1629
|
+
return verifySignature(this.productPublicKey, message, signature);
|
|
1630
|
+
}
|
|
1631
|
+
// File persistence methods
|
|
1632
|
+
opsFileFolderPath() {
|
|
1633
|
+
return path.join(this.userResourceBaseDirectory, this.productName);
|
|
1634
|
+
}
|
|
1635
|
+
opsFilePath() {
|
|
1636
|
+
return path.join(this.opsFileFolderPath(), `${this.productName}.ops`);
|
|
1637
|
+
}
|
|
1638
|
+
ltsFilePath() {
|
|
1639
|
+
return path.join(this.opsFileFolderPath(), `${this.productName}.lts`);
|
|
1640
|
+
}
|
|
1641
|
+
persistActivationOnDisk(activationResponseJson) {
|
|
1642
|
+
safeWriteJSON(this.opsFilePath(), activationResponseJson);
|
|
1643
|
+
}
|
|
1644
|
+
persistTrialActivationOnDisk(activationResponseJson) {
|
|
1645
|
+
safeWriteJSON(this.opsFilePath(), activationResponseJson);
|
|
1646
|
+
}
|
|
1647
|
+
loadActivationFromDisk() {
|
|
1648
|
+
const opsFileJson = safeReadJSON(this.opsFilePath());
|
|
1649
|
+
if (!opsFileJson || !opsFileJson.signature) {
|
|
1650
|
+
return undefined;
|
|
1651
|
+
}
|
|
1652
|
+
// Detect version 5 vs version 6 format
|
|
1653
|
+
if (opsFileJson.version === 5) {
|
|
1654
|
+
// Version 5 format: map fields to version 6 structure
|
|
1655
|
+
if (!opsFileJson.license_key || !opsFileJson.fingerprint) {
|
|
1656
|
+
return undefined;
|
|
1657
|
+
}
|
|
1658
|
+
return {
|
|
1659
|
+
type: types_1.LicenseType.Standard, // Assume Standard license for version 5
|
|
1660
|
+
serial: opsFileJson.license_key,
|
|
1661
|
+
signature: opsFileJson.signature,
|
|
1662
|
+
expires: opsFileJson.expires || null,
|
|
1663
|
+
hardware_fingerprint: opsFileJson.fingerprint,
|
|
1664
|
+
hardware_fingerprint_version: opsFileJson.hwfp_version || 2,
|
|
1665
|
+
signature_seed_version: 5,
|
|
1666
|
+
// Required fields with null values for version 5 compatibility
|
|
1667
|
+
user_id: null,
|
|
1668
|
+
user_email: opsFileJson.user_email || null,
|
|
1669
|
+
product_id: this.productId,
|
|
1670
|
+
product_name: this.productName,
|
|
1671
|
+
manufacturer_id: null,
|
|
1672
|
+
manufacturer_name: null,
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
else {
|
|
1676
|
+
// Version 6+ format: use current structure
|
|
1677
|
+
if (!opsFileJson.serial) {
|
|
1678
|
+
return undefined;
|
|
1679
|
+
}
|
|
1680
|
+
return opsFileJson;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
loadTrialActivationFromDisk() {
|
|
1684
|
+
const opsFileJson = safeReadJSON(this.opsFilePath());
|
|
1685
|
+
if (!opsFileJson || !opsFileJson.signature) {
|
|
1686
|
+
return undefined;
|
|
1687
|
+
}
|
|
1688
|
+
// Detect version 5 vs version 6 format
|
|
1689
|
+
if (opsFileJson.version === 5) {
|
|
1690
|
+
// Version 5 trial format: map fields to version 6 structure
|
|
1691
|
+
if (!opsFileJson.timestamp || opsFileJson.trialDays == null) {
|
|
1692
|
+
return undefined;
|
|
1693
|
+
}
|
|
1694
|
+
return {
|
|
1695
|
+
signature: opsFileJson.signature,
|
|
1696
|
+
timestamp: Number(opsFileJson.timestamp),
|
|
1697
|
+
trial_days: Number(opsFileJson.trialDays),
|
|
1698
|
+
hardware_fingerprint: opsFileJson.fingerprint || this.hwfpUtil.getFingerprint(),
|
|
1699
|
+
hardware_fingerprint_version: opsFileJson.hwfp_version || 2,
|
|
1700
|
+
signature_seed_version: 5,
|
|
1701
|
+
// Required fields with null values for version 5 compatibility
|
|
1702
|
+
product_id: this.productId,
|
|
1703
|
+
product_name: this.productName,
|
|
1704
|
+
manufacturer_id: null,
|
|
1705
|
+
manufacturer_name: null,
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
else {
|
|
1709
|
+
// Version 6+ format: use current structure
|
|
1710
|
+
if (!opsFileJson.timestamp || opsFileJson.trial_days == null) {
|
|
1711
|
+
return undefined;
|
|
1712
|
+
}
|
|
1713
|
+
return opsFileJson;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
updateLastTrialLaunchTime() {
|
|
1717
|
+
try {
|
|
1718
|
+
const payload = { lts: Date.now() };
|
|
1719
|
+
safeWriteJSON(this.ltsFilePath(), payload);
|
|
1720
|
+
}
|
|
1721
|
+
catch (ex) {
|
|
1722
|
+
if (!this.isTestEnvironment) {
|
|
1723
|
+
console.error(`Could not update last trial launch time: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
readLastTrialLaunchTime() {
|
|
1728
|
+
try {
|
|
1729
|
+
const ltsFileJson = safeReadJSON(this.ltsFilePath());
|
|
1730
|
+
if (!ltsFileJson || !ltsFileJson.lts) {
|
|
1731
|
+
return undefined;
|
|
1732
|
+
}
|
|
1733
|
+
return ltsFileJson.lts;
|
|
1734
|
+
}
|
|
1735
|
+
catch (ex) {
|
|
1736
|
+
if (!this.isTestEnvironment) {
|
|
1737
|
+
console.error(`Could not read last trial launch time: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1738
|
+
}
|
|
1739
|
+
return undefined;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Check for product updates from the Product Hub API.
|
|
1744
|
+
*
|
|
1745
|
+
* This method queries the latest version available in the Release distribution channel
|
|
1746
|
+
* and compares it with the currently installed version. Results are cached and can
|
|
1747
|
+
* be accessed via `isUpdateAvailable()`, `getUpdateReferralURL()`, and `getUpdateFiles()`.
|
|
1748
|
+
*
|
|
1749
|
+
* The method is automatically called once during construction but can be called again
|
|
1750
|
+
* to re-check for updates (e.g., if the initial check failed due to network issues).
|
|
1751
|
+
*
|
|
1752
|
+
* @returns Promise that resolves to `true` if the check succeeded, `false` if it failed
|
|
1753
|
+
*
|
|
1754
|
+
* @example
|
|
1755
|
+
* ```typescript
|
|
1756
|
+
* // Manual re-check for updates
|
|
1757
|
+
* const success = await client.checkForUpdates();
|
|
1758
|
+
* if (success && client.isUpdateAvailable()) {
|
|
1759
|
+
* console.log("Update available!");
|
|
1760
|
+
* }
|
|
1761
|
+
* ```
|
|
1762
|
+
*/
|
|
1763
|
+
async checkForUpdates() {
|
|
1764
|
+
// If no version was provided, update checking is disabled
|
|
1765
|
+
if (!this.currentVersion) {
|
|
1766
|
+
this.updateCheckResult = {
|
|
1767
|
+
success: false,
|
|
1768
|
+
updateAvailable: false,
|
|
1769
|
+
latestBuild: null,
|
|
1770
|
+
updateReferralUrl: null,
|
|
1771
|
+
};
|
|
1772
|
+
return false;
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
// Get latest version from API
|
|
1776
|
+
const response = await this.apiClient.getLatestProductVersion();
|
|
1777
|
+
// Detect current OS
|
|
1778
|
+
const currentOs = this.detectOperatingSystem();
|
|
1779
|
+
// Find matching build for our OS
|
|
1780
|
+
const matchingBuild = this.selectBuildForOS(response.builds, currentOs);
|
|
1781
|
+
if (!matchingBuild) {
|
|
1782
|
+
// No build available for this OS
|
|
1783
|
+
this.updateCheckResult = {
|
|
1784
|
+
success: true,
|
|
1785
|
+
updateAvailable: false,
|
|
1786
|
+
latestBuild: null,
|
|
1787
|
+
updateReferralUrl: response.update_referral_url,
|
|
1788
|
+
};
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
// Compare versions
|
|
1792
|
+
const updateAvailable = this.isNewerVersion(matchingBuild);
|
|
1793
|
+
this.updateCheckResult = {
|
|
1794
|
+
success: true,
|
|
1795
|
+
updateAvailable,
|
|
1796
|
+
latestBuild: matchingBuild,
|
|
1797
|
+
updateReferralUrl: response.update_referral_url,
|
|
1798
|
+
};
|
|
1799
|
+
return true;
|
|
1800
|
+
}
|
|
1801
|
+
catch (ex) {
|
|
1802
|
+
if (!this.isTestEnvironment) {
|
|
1803
|
+
console.error(`Update check failed: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1804
|
+
}
|
|
1805
|
+
this.updateCheckResult = {
|
|
1806
|
+
success: false,
|
|
1807
|
+
updateAvailable: false,
|
|
1808
|
+
latestBuild: null,
|
|
1809
|
+
updateReferralUrl: null,
|
|
1810
|
+
};
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Check if an update is available for this product.
|
|
1816
|
+
*
|
|
1817
|
+
* @returns `true` if an update is available, `false` otherwise
|
|
1818
|
+
*
|
|
1819
|
+
* @remarks
|
|
1820
|
+
* Returns `false` if the update check hasn't been performed yet, failed,
|
|
1821
|
+
* or if no update is available.
|
|
1822
|
+
*
|
|
1823
|
+
* @example
|
|
1824
|
+
* ```typescript
|
|
1825
|
+
* if (client.isUpdateAvailable()) {
|
|
1826
|
+
* console.log("New version available!");
|
|
1827
|
+
* console.log("Download from:", client.getUpdateReferralURL());
|
|
1828
|
+
* }
|
|
1829
|
+
* ```
|
|
1830
|
+
*/
|
|
1831
|
+
isUpdateAvailable() {
|
|
1832
|
+
return this.updateCheckResult?.updateAvailable ?? false;
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Get the update referral URL for this product.
|
|
1836
|
+
*
|
|
1837
|
+
* This URL can be used to direct users to a download page or update instructions.
|
|
1838
|
+
* The URL is configured per-product in the Product Hub web interface.
|
|
1839
|
+
*
|
|
1840
|
+
* @returns The update referral URL, or `null` if not available or check failed
|
|
1841
|
+
*
|
|
1842
|
+
* @example
|
|
1843
|
+
* ```typescript
|
|
1844
|
+
* const url = client.getUpdateReferralURL();
|
|
1845
|
+
* if (url) {
|
|
1846
|
+
* // Open URL in browser or show to user
|
|
1847
|
+
* console.log("Visit:", url);
|
|
1848
|
+
* }
|
|
1849
|
+
* ```
|
|
1850
|
+
*/
|
|
1851
|
+
getUpdateReferralURL() {
|
|
1852
|
+
return this.updateCheckResult?.updateReferralUrl ?? null;
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Get the downloadable files for the latest product update.
|
|
1856
|
+
*
|
|
1857
|
+
* Returns an array of files (installers, packages, etc.) that can be downloaded
|
|
1858
|
+
* to update the product. Files are specific to the current operating system.
|
|
1859
|
+
*
|
|
1860
|
+
* @returns Promise that resolves to an array of build files, or empty array if unavailable
|
|
1861
|
+
*
|
|
1862
|
+
* @remarks
|
|
1863
|
+
* - Returns empty array if update check failed or no update is available
|
|
1864
|
+
* - Files are automatically filtered for the current operating system
|
|
1865
|
+
* - Each file includes: id, name, size, sha256 hash, and download URL
|
|
1866
|
+
*
|
|
1867
|
+
* @example
|
|
1868
|
+
* ```typescript
|
|
1869
|
+
* const files = await client.getUpdateFiles();
|
|
1870
|
+
* for (const file of files) {
|
|
1871
|
+
* console.log(`Download: ${file.name} (${file.size} bytes)`);
|
|
1872
|
+
* console.log(`URL: ${file.url}`);
|
|
1873
|
+
* console.log(`SHA256: ${file.sha256}`);
|
|
1874
|
+
* }
|
|
1875
|
+
* ```
|
|
1876
|
+
*/
|
|
1877
|
+
async getUpdateFiles() {
|
|
1878
|
+
if (!this.updateCheckResult?.success ||
|
|
1879
|
+
!this.updateCheckResult?.updateAvailable ||
|
|
1880
|
+
!this.updateCheckResult?.latestBuild) {
|
|
1881
|
+
return [];
|
|
1882
|
+
}
|
|
1883
|
+
try {
|
|
1884
|
+
const buildId = this.updateCheckResult.latestBuild.id;
|
|
1885
|
+
return await this.apiClient.getBuildFiles(buildId);
|
|
1886
|
+
}
|
|
1887
|
+
catch (ex) {
|
|
1888
|
+
if (!this.isTestEnvironment) {
|
|
1889
|
+
console.error(`Failed to fetch update files: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1890
|
+
}
|
|
1891
|
+
return [];
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Detect the current operating system.
|
|
1896
|
+
* @private
|
|
1897
|
+
*/
|
|
1898
|
+
detectOperatingSystem() {
|
|
1899
|
+
const platform = process.platform;
|
|
1900
|
+
if (platform === 'darwin') {
|
|
1901
|
+
return 'macOS';
|
|
1902
|
+
}
|
|
1903
|
+
else if (platform === 'win32') {
|
|
1904
|
+
return 'Windows';
|
|
1905
|
+
}
|
|
1906
|
+
// Something is wrong here - unsupported platform
|
|
1907
|
+
if (!this.isTestEnvironment) {
|
|
1908
|
+
console.error(`Unrecognized platform "${platform}" - defaulting to "unknown" OS for update checks`);
|
|
1909
|
+
}
|
|
1910
|
+
return 'unknown';
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Select the appropriate build for the given OS.
|
|
1914
|
+
* Prefers exact OS match over "any".
|
|
1915
|
+
* @private
|
|
1916
|
+
*/
|
|
1917
|
+
selectBuildForOS(builds, os) {
|
|
1918
|
+
// First try to find exact OS match
|
|
1919
|
+
const exactMatch = builds.find((b) => b.os === os);
|
|
1920
|
+
if (exactMatch) {
|
|
1921
|
+
return exactMatch;
|
|
1922
|
+
}
|
|
1923
|
+
// Fall back to "any" OS
|
|
1924
|
+
const anyMatch = builds.find((b) => b.os === 'any');
|
|
1925
|
+
if (anyMatch) {
|
|
1926
|
+
return anyMatch;
|
|
1927
|
+
}
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Check if the remote build version is newer than the current version.
|
|
1932
|
+
* @private
|
|
1933
|
+
*/
|
|
1934
|
+
isNewerVersion(remoteBuild) {
|
|
1935
|
+
// This method is only called after we've verified currentVersion is not null
|
|
1936
|
+
if (!this.currentVersion)
|
|
1937
|
+
return false;
|
|
1938
|
+
// Parse current version (format: "major.minor.revision")
|
|
1939
|
+
const currentParts = this.currentVersion.split('.').map((p) => parseInt(p, 10));
|
|
1940
|
+
const currentMajor = currentParts[0] || 0;
|
|
1941
|
+
const currentMinor = currentParts[1] || 0;
|
|
1942
|
+
const currentRevision = currentParts[2] || 0;
|
|
1943
|
+
const currentBuild = this.currentBuildNumber ?? 0;
|
|
1944
|
+
const remoteMajor = remoteBuild.major;
|
|
1945
|
+
const remoteMinor = remoteBuild.minor;
|
|
1946
|
+
const remoteRevision = remoteBuild.revision;
|
|
1947
|
+
const remoteBuildNum = remoteBuild.build_number ?? 0;
|
|
1948
|
+
// Compare major.minor.revision first
|
|
1949
|
+
if (remoteMajor > currentMajor)
|
|
1950
|
+
return true;
|
|
1951
|
+
if (remoteMajor < currentMajor)
|
|
1952
|
+
return false;
|
|
1953
|
+
if (remoteMinor > currentMinor)
|
|
1954
|
+
return true;
|
|
1955
|
+
if (remoteMinor < currentMinor)
|
|
1956
|
+
return false;
|
|
1957
|
+
if (remoteRevision > currentRevision)
|
|
1958
|
+
return true;
|
|
1959
|
+
if (remoteRevision < currentRevision)
|
|
1960
|
+
return false;
|
|
1961
|
+
// If major.minor.revision are equal, compare build numbers
|
|
1962
|
+
return remoteBuildNum > currentBuild;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
ProductHub.AudioPluginClient = AudioPluginClient;
|
|
1966
|
+
})(ProductHub || (exports.ProductHub = ProductHub = {}));
|
|
1967
|
+
// --------------- Internal Helper Functions ---------------
|
|
1968
|
+
/**
|
|
1969
|
+
* @internal
|
|
1970
|
+
* Detect if we're running in a test environment.
|
|
1971
|
+
* Checks multiple indicators commonly used by test frameworks.
|
|
1972
|
+
*/
|
|
1973
|
+
function isTestEnvironment() {
|
|
1974
|
+
return (process.env.NODE_ENV === 'test' ||
|
|
1975
|
+
typeof process.env.JEST_WORKER_ID !== 'undefined' ||
|
|
1976
|
+
typeof global.it === 'function');
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* @internal
|
|
1980
|
+
* Verify RSA-SHA256 signature using Node.js crypto module.
|
|
1981
|
+
*/
|
|
1982
|
+
function verifySignature(publicKey, message, signatureB64) {
|
|
1983
|
+
try {
|
|
1984
|
+
const verifier = (0, crypto_1.createVerify)('RSA-SHA256');
|
|
1985
|
+
verifier.update(message);
|
|
1986
|
+
verifier.end();
|
|
1987
|
+
return verifier.verify(publicKey, new Uint8Array(Buffer.from(signatureB64, 'base64')));
|
|
1988
|
+
}
|
|
1989
|
+
catch (ex) {
|
|
1990
|
+
if (!isTestEnvironment()) {
|
|
1991
|
+
console.error(`Could not verify signature: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
1992
|
+
}
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* @internal
|
|
1998
|
+
* Safely write JSON data to file with automatic directory creation.
|
|
1999
|
+
*/
|
|
2000
|
+
function safeWriteJSON(filePath, data) {
|
|
2001
|
+
try {
|
|
2002
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2003
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
2004
|
+
}
|
|
2005
|
+
catch (ex) {
|
|
2006
|
+
if (!isTestEnvironment()) {
|
|
2007
|
+
console.error(`Could not write JSON file to ${filePath}: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* @internal
|
|
2013
|
+
* Safely read and parse JSON data from file.
|
|
2014
|
+
*/
|
|
2015
|
+
function safeReadJSON(filePath) {
|
|
2016
|
+
try {
|
|
2017
|
+
if (!fs.existsSync(filePath))
|
|
2018
|
+
return undefined;
|
|
2019
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
2020
|
+
return JSON.parse(raw);
|
|
2021
|
+
}
|
|
2022
|
+
catch (ex) {
|
|
2023
|
+
if (!isTestEnvironment()) {
|
|
2024
|
+
console.error(`Could not read JSON file from ${filePath}: ${ex?.message || ex}\n\nStack trace:\n${ex?.stack || ''}`);
|
|
2025
|
+
}
|
|
2026
|
+
return undefined;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* @internal
|
|
2031
|
+
* Validate if a string has any lines with leading whitespace.
|
|
2032
|
+
*/
|
|
2033
|
+
function hasLeadingWhitespaceLines(str) {
|
|
2034
|
+
const lines = str.split('\n');
|
|
2035
|
+
return lines.some((line) => /^\s+/.test(line));
|
|
2036
|
+
}
|