@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.
@@ -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
+ }