@licenseseat/js 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -13,6 +13,8 @@ The official JavaScript/TypeScript SDK for [LicenseSeat](https://licenseseat.com
13
13
  - **License activation & deactivation** – Activate licenses with automatic device fingerprinting
14
14
  - **Online & offline validation** – Validate licenses with optional offline fallback
15
15
  - **Entitlement checking** – Check feature access with `hasEntitlement()` and `checkEntitlement()`
16
+ - **Heartbeat** – Automatic periodic heartbeats to report device activity
17
+ - **Telemetry** – Auto-collected device and environment data sent with each API request
16
18
  - **Local caching** – Secure localStorage-based caching with clock tamper detection
17
19
  - **Auto-retry with exponential backoff** – Resilient network handling
18
20
  - **Event-driven architecture** – Subscribe to SDK lifecycle events
@@ -138,6 +140,14 @@ const sdk = new LicenseSeat({
138
140
  autoValidateInterval: 3600000, // 1 hour (in ms)
139
141
  autoInitialize: true, // Auto-validate cached license on init
140
142
 
143
+ // Heartbeat
144
+ heartbeatInterval: 300000, // 5 minutes (in ms), 0 to disable
145
+
146
+ // Telemetry
147
+ telemetryEnabled: true, // Set false to disable (e.g. GDPR)
148
+ appVersion: '1.2.0', // Your app version (sent in telemetry)
149
+ appBuild: '42', // Your app build number (sent in telemetry)
150
+
141
151
  // Offline Support
142
152
  offlineFallbackEnabled: false, // Enable offline validation fallback
143
153
  maxOfflineDays: 0, // Max days offline (0 = disabled)
@@ -164,6 +174,10 @@ const sdk = new LicenseSeat({
164
174
  | `storagePrefix` | `string` | `'licenseseat_'` | Prefix for localStorage keys |
165
175
  | `autoValidateInterval` | `number` | `3600000` | Auto-validation interval in ms (1 hour) |
166
176
  | `autoInitialize` | `boolean` | `true` | Auto-initialize and validate cached license |
177
+ | `heartbeatInterval` | `number` | `300000` | Heartbeat interval in ms (5 minutes). Set `0` to disable |
178
+ | `telemetryEnabled` | `boolean` | `true` | Enable telemetry collection. Set `false` for GDPR compliance |
179
+ | `appVersion` | `string` | `null` | Your app version string (sent as `app_version` in telemetry) |
180
+ | `appBuild` | `string` | `null` | Your app build identifier (sent as `app_build` in telemetry) |
167
181
  | `offlineFallbackEnabled` | `boolean` | `false` | Enable offline validation on network errors |
168
182
  | `maxOfflineDays` | `number` | `0` | Maximum days license works offline (0 = disabled) |
169
183
  | `maxRetries` | `number` | `3` | Max retry attempts for failed API calls |
@@ -248,9 +262,11 @@ console.log(result);
248
262
 
249
263
  ### Entitlement Methods
250
264
 
265
+ > **Note:** Entitlements are optional. A license may have zero entitlements if the associated plan has no entitlements configured. The `active_entitlements` array may be empty or the field may be undefined/null.
266
+
251
267
  #### `sdk.hasEntitlement(key)`
252
268
 
253
- Check if an entitlement is active. Returns a simple boolean.
269
+ Check if an entitlement is active. Returns a simple boolean. Returns `false` if no entitlements exist.
254
270
 
255
271
  ```javascript
256
272
  if (sdk.hasEntitlement('pro')) {
@@ -308,17 +324,38 @@ console.log(status);
308
324
 
309
325
  #### `sdk.testAuth()`
310
326
 
311
- Test API authentication (useful for verifying API key).
327
+ Test API connectivity by calling the `/health` endpoint. Returns health status and API version.
312
328
 
313
329
  ```javascript
314
330
  try {
315
331
  const result = await sdk.testAuth();
316
- console.log('Authenticated:', result.authenticated);
332
+ console.log('Authenticated:', result.authenticated); // Always true if request succeeds
333
+ console.log('Healthy:', result.healthy); // API health status
334
+ console.log('API Version:', result.api_version); // e.g., '1.0.0'
317
335
  } catch (error) {
318
- console.error('Auth failed:', error);
336
+ console.error('Connection failed:', error);
319
337
  }
320
338
  ```
321
339
 
340
+ > **Note:** This method tests API connectivity, not API key validity. A successful response means the API is reachable. Authentication errors will surface when calling protected endpoints like `activate()` or `validateLicense()`.
341
+
342
+ #### `sdk.heartbeat()`
343
+
344
+ Send a heartbeat to report that the current device is still active. Heartbeats are sent automatically at the configured `heartbeatInterval`, but you can also send one manually.
345
+
346
+ ```javascript
347
+ try {
348
+ const result = await sdk.heartbeat();
349
+ console.log('Heartbeat received at:', result.received_at);
350
+ } catch (error) {
351
+ console.error('Heartbeat failed:', error);
352
+ }
353
+ ```
354
+
355
+ Returns `undefined` if no active license is cached. When auto-heartbeat is enabled (the default), the SDK sends heartbeats every 5 minutes while a license is active. Auto-heartbeat starts automatically after `activate()` or when the SDK initializes with a cached license.
356
+
357
+ To disable auto-heartbeat, set `heartbeatInterval: 0` in the configuration.
358
+
322
359
  #### `sdk.reset()`
323
360
 
324
361
  Clear all cached data and reset SDK state.
@@ -397,6 +434,9 @@ sdk.off('activation:success', handler);
397
434
  | **Auto-Validation** | | |
398
435
  | `autovalidation:cycle` | Auto-validation scheduled | `{ nextRunAt: Date }` |
399
436
  | `autovalidation:stopped` | Auto-validation stopped | – |
437
+ | **Heartbeat** | | |
438
+ | `heartbeat:success` | Heartbeat acknowledged by server | `HeartbeatResponse` |
439
+ | `heartbeat:cycle` | Auto-heartbeat tick completed | `{ nextRunAt: Date }` |
400
440
  | **Network** | | |
401
441
  | `network:online` | Network connectivity restored | – |
402
442
  | `network:offline` | Network connectivity lost | `{ error }` |
@@ -465,6 +505,70 @@ if (result.offline) {
465
505
  3. When offline, the SDK verifies the signature locally
466
506
  4. Clock tamper detection prevents users from bypassing expiration
467
507
 
508
+ ### Offline Methods
509
+
510
+ #### `sdk.syncOfflineAssets()`
511
+
512
+ Fetches the offline token and signing key from the server. Uses the currently cached license. Call this after activation to prepare for offline usage.
513
+
514
+ ```javascript
515
+ // First activate (caches the license)
516
+ await sdk.activate('LICENSE-KEY');
517
+
518
+ // Then sync offline assets (uses cached license)
519
+ const assets = await sdk.syncOfflineAssets();
520
+ console.log('Offline token key ID:', assets.kid);
521
+ console.log('Expires at:', assets.exp_at);
522
+ ```
523
+
524
+ #### `sdk.getOfflineToken()`
525
+
526
+ Fetches a signed offline token for the currently cached license. Returns the token structure containing the license data and Ed25519 signature.
527
+
528
+ ```javascript
529
+ // Must have an active license cached first
530
+ const token = await sdk.getOfflineToken();
531
+ console.log(token);
532
+ // {
533
+ // object: 'offline_token',
534
+ // token: { license_key, product_slug, plan_key, ... },
535
+ // signature: { algorithm: 'Ed25519', key_id, value },
536
+ // canonical: '...'
537
+ // }
538
+ ```
539
+
540
+ #### `sdk.getSigningKey(keyId)`
541
+
542
+ Fetches the Ed25519 public key used for verifying offline token signatures.
543
+
544
+ ```javascript
545
+ const signingKey = await sdk.getSigningKey('key-id-001');
546
+ console.log(signingKey);
547
+ // {
548
+ // object: 'signing_key',
549
+ // kid: 'key-id-001',
550
+ // public_key: 'base64-encoded-public-key',
551
+ // algorithm: 'Ed25519',
552
+ // created_at: '2024-01-01T00:00:00Z'
553
+ // }
554
+ ```
555
+
556
+ #### `sdk.verifyOfflineToken(token, publicKeyB64)`
557
+
558
+ Verifies an offline token's Ed25519 signature locally. **Both parameters are required.**
559
+
560
+ ```javascript
561
+ // Fetch the token and signing key first
562
+ const token = await sdk.getOfflineToken();
563
+ const signingKey = await sdk.getSigningKey(token.signature.key_id);
564
+
565
+ // Verify the signature
566
+ const isValid = await sdk.verifyOfflineToken(token, signingKey.public_key);
567
+ console.log('Signature valid:', isValid);
568
+ ```
569
+
570
+ > **Important:** The `verifyOfflineToken()` method requires you to pass both the token and the public key. Fetch the signing key using `getSigningKey()` with the `key_id` from the token's signature.
571
+
468
572
  ### Offline Token Structure
469
573
 
470
574
  ```javascript
@@ -498,6 +602,126 @@ if (result.offline) {
498
602
 
499
603
  ---
500
604
 
605
+ ## Telemetry
606
+
607
+ The SDK automatically collects non-PII (non-personally-identifiable) device and environment data and includes it with every POST request sent to the LicenseSeat API. This data helps you understand what platforms and environments your customers use.
608
+
609
+ Telemetry is **enabled by default** and can be disabled at any time.
610
+
611
+ ### Collected Fields
612
+
613
+ | Field | Type | Example | Description |
614
+ | -------------------- | -------- | ---------------------- | ------------------------------------------------ |
615
+ | `sdk_name` | `string` | `"js"` | Always `"js"` for this SDK |
616
+ | `sdk_version` | `string` | `"0.4.0"` | SDK version |
617
+ | `os_name` | `string` | `"macOS"` | Operating system name |
618
+ | `os_version` | `string` | `"14.2.1"` | Operating system version |
619
+ | `platform` | `string` | `"browser"` | Runtime platform (`browser`, `node`, `electron`, `react-native`, `deno`, `bun`) |
620
+ | `device_model` | `string` | `null` | Device model (Chromium userAgentData only) |
621
+ | `device_type` | `string` | `"desktop"` | Device type (`desktop`, `phone`, `tablet`, `server`) |
622
+ | `locale` | `string` | `"en-US"` | Full locale string |
623
+ | `timezone` | `string` | `"America/New_York"` | IANA timezone |
624
+ | `language` | `string` | `"en"` | 2-letter language code |
625
+ | `architecture` | `string` | `"arm64"` | CPU architecture |
626
+ | `cpu_cores` | `number` | `10` | Number of logical CPU cores |
627
+ | `memory_gb` | `number` | `16` | Approximate RAM in GB (Chrome/Node.js only) |
628
+ | `screen_resolution` | `string` | `"1920x1080"` | Screen resolution |
629
+ | `display_scale` | `number` | `2` | Device pixel ratio |
630
+ | `browser_name` | `string` | `"Chrome"` | Browser name (browser environments only) |
631
+ | `browser_version` | `string` | `"123.0"` | Browser version (browser environments only) |
632
+ | `runtime_version` | `string` | `"20.11.0"` | Runtime version (Node.js, Deno, Bun, Electron) |
633
+ | `app_version` | `string` | `"1.2.0"` | Your app version (from `appVersion` config) |
634
+ | `app_build` | `string` | `"42"` | Your app build (from `appBuild` config) |
635
+
636
+ Fields that cannot be detected in the current environment are omitted (not sent as `null`).
637
+
638
+ ### Providing App Version
639
+
640
+ Pass your own app version and build number via configuration so they appear in telemetry:
641
+
642
+ ```javascript
643
+ const sdk = new LicenseSeat({
644
+ apiKey: 'your-key',
645
+ productSlug: 'your-product',
646
+ appVersion: '1.2.0',
647
+ appBuild: '42'
648
+ });
649
+ ```
650
+
651
+ ### Disabling Telemetry
652
+
653
+ To disable telemetry collection entirely (for example, to comply with GDPR or other privacy regulations):
654
+
655
+ ```javascript
656
+ const sdk = new LicenseSeat({
657
+ apiKey: 'your-key',
658
+ productSlug: 'your-product',
659
+ telemetryEnabled: false
660
+ });
661
+ ```
662
+
663
+ When telemetry is disabled, no device or environment data is attached to API requests.
664
+
665
+ ### Privacy
666
+
667
+ Telemetry collects only non-personally-identifiable information. No IP addresses, user names, email addresses, or other PII are collected by the SDK. The data is used solely to help you understand the platforms and environments where your software is used.
668
+
669
+ Telemetry is opt-out: set `telemetryEnabled: false` to disable it completely.
670
+
671
+ ---
672
+
673
+ ## Heartbeat
674
+
675
+ The SDK sends periodic heartbeat signals to let the server know a device is still actively using the license. This enables usage analytics and helps detect inactive seats.
676
+
677
+ ### How It Works
678
+
679
+ - After a license is activated (or when the SDK initializes with a cached license), a heartbeat timer starts automatically.
680
+ - The default interval is **5 minutes** (`300000` ms), configurable via `heartbeatInterval`.
681
+ - Each heartbeat sends the current `device_id` to the server.
682
+ - Heartbeats also run alongside auto-validation cycles.
683
+
684
+ ### Manual Heartbeat
685
+
686
+ You can send a heartbeat at any time:
687
+
688
+ ```javascript
689
+ await sdk.heartbeat();
690
+ ```
691
+
692
+ ### Configuring the Interval
693
+
694
+ ```javascript
695
+ const sdk = new LicenseSeat({
696
+ apiKey: 'your-key',
697
+ productSlug: 'your-product',
698
+ heartbeatInterval: 600000 // 10 minutes
699
+ });
700
+ ```
701
+
702
+ Set `heartbeatInterval: 0` to disable auto-heartbeat entirely. You can still call `sdk.heartbeat()` manually.
703
+
704
+ ### Heartbeat Events
705
+
706
+ | Event | Description | Data |
707
+ | -------------------- | ---------------------------------- | ----------------------- |
708
+ | `heartbeat:success` | Heartbeat acknowledged by server | `HeartbeatResponse` |
709
+ | `heartbeat:cycle` | Auto-heartbeat tick completed | `{ nextRunAt: Date }` |
710
+
711
+ ```javascript
712
+ sdk.on('heartbeat:success', (data) => {
713
+ console.log('Heartbeat received at:', data.received_at);
714
+ });
715
+ ```
716
+
717
+ ### Heartbeat Lifecycle
718
+
719
+ - **Starts** automatically after `sdk.activate()` succeeds, or on SDK init if a cached license exists.
720
+ - **Stops** automatically when `sdk.deactivate()`, `sdk.reset()`, or `sdk.destroy()` is called.
721
+ - Heartbeat failures are logged (in debug mode) but do not throw or interrupt the SDK.
722
+
723
+ ---
724
+
501
725
  ## Error Handling
502
726
 
503
727
  The SDK exports custom error classes for precise error handling:
@@ -564,7 +788,39 @@ Common error codes:
564
788
 
565
789
  - **Modern browsers**: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+
566
790
  - **Bundlers**: Vite, Webpack, Rollup, esbuild, Parcel
567
- - **Node.js**: 18+ (requires polyfills for `localStorage`, `document`)
791
+ - **Node.js**: 18+ (requires polyfills - see below)
792
+
793
+ ### Node.js Usage
794
+
795
+ The SDK is designed for browsers but works in Node.js with polyfills. Add these before importing the SDK:
796
+
797
+ ```javascript
798
+ // Required polyfills for Node.js
799
+ const storage = {};
800
+ globalThis.localStorage = {
801
+ getItem(key) { return Object.prototype.hasOwnProperty.call(storage, key) ? storage[key] : null; },
802
+ setItem(key, value) { storage[key] = String(value); },
803
+ removeItem(key) { delete storage[key]; },
804
+ clear() { for (const key in storage) delete storage[key]; },
805
+ };
806
+
807
+ // Override Object.keys to support localStorage iteration (used by cache.getAllKeys())
808
+ const originalKeys = Object.keys;
809
+ Object.keys = function(obj) {
810
+ if (obj === globalThis.localStorage) return originalKeys(storage);
811
+ return originalKeys(obj);
812
+ };
813
+
814
+ // Device fingerprinting polyfills (provides stable fallback values)
815
+ globalThis.document = { createElement: () => ({ getContext: () => null }), querySelector: () => null };
816
+ globalThis.window = { navigator: {}, screen: {} };
817
+ globalThis.navigator = { userAgent: 'Node.js', language: 'en', hardwareConcurrency: 4 };
818
+
819
+ // Now import the SDK
820
+ const { default: LicenseSeat } = await import('@licenseseat/js');
821
+ ```
822
+
823
+ > **Note:** In Node.js, device fingerprinting will use fallback values since browser APIs aren't available. For consistent device identification across restarts, pass an explicit `deviceId` to `activate()`.
568
824
 
569
825
  ---
570
826
 
@@ -680,6 +936,50 @@ npm install
680
936
  | `npm run test:coverage` | Run tests with coverage report |
681
937
  | `npm run typecheck` | Type-check without emitting |
682
938
 
939
+ ### Integration Tests
940
+
941
+ The SDK includes comprehensive integration tests that run against the live LicenseSeat API. These tests verify real-world functionality including activation, validation, deactivation, and offline cryptographic operations.
942
+
943
+ #### Running Integration Tests (Node.js)
944
+
945
+ ```bash
946
+ # Set environment variables
947
+ export LICENSESEAT_API_KEY="ls_your_api_key_here"
948
+ export LICENSESEAT_PRODUCT_SLUG="your-product"
949
+ export LICENSESEAT_LICENSE_KEY="YOUR-LICENSE-KEY"
950
+
951
+ # Run the tests
952
+ node test-live.mjs
953
+ ```
954
+
955
+ Or with inline environment variables:
956
+
957
+ ```bash
958
+ LICENSESEAT_API_KEY=ls_xxx LICENSESEAT_PRODUCT_SLUG=my-app LICENSESEAT_LICENSE_KEY=XXX-XXX node test-live.mjs
959
+ ```
960
+
961
+ #### Running Integration Tests (Browser)
962
+
963
+ Open `test-live.html` in a browser. You'll be prompted to enter your credentials:
964
+
965
+ 1. **API Key** - Your LicenseSeat API key (starts with `ls_`)
966
+ 2. **Product Slug** - Your product identifier
967
+ 3. **License Key** - A valid license key for testing
968
+
969
+ Credentials are stored in `localStorage` for convenience during development.
970
+
971
+ #### What the Integration Tests Cover
972
+
973
+ | Category | Tests |
974
+ |----------|-------|
975
+ | **Initialization** | SDK setup, configuration defaults |
976
+ | **Activation** | License activation, device ID generation |
977
+ | **Validation** | Online validation, entitlement checking |
978
+ | **Deactivation** | License deactivation, cache clearing |
979
+ | **Offline Crypto** | Ed25519 signature verification, offline token fetching, tamper detection |
980
+ | **Error Handling** | Invalid licenses, missing config |
981
+ | **Singleton** | Shared instance pattern |
982
+
683
983
  ### Project Structure
684
984
 
685
985
  ```
@@ -687,15 +987,18 @@ licenseseat-js/
687
987
  ├── src/
688
988
  │ ├── index.js # Entry point, exports
689
989
  │ ├── LicenseSeat.js # Main SDK class
990
+ │ ├── telemetry.js # Telemetry collection (device/environment data)
690
991
  │ ├── cache.js # LicenseCache (localStorage)
691
992
  │ ├── errors.js # Error classes
692
993
  │ ├── types.js # JSDoc type definitions
693
994
  │ └── utils.js # Utility functions
694
- ├── tests/
995
+ ├── tests/ # Unit tests (mocked API)
695
996
  │ ├── setup.js # Test setup
696
997
  │ ├── mocks/ # MSW handlers
697
998
  │ ├── LicenseSeat.test.js
698
999
  │ └── utils.test.js
1000
+ ├── test-live.mjs # Integration tests (Node.js)
1001
+ ├── test-live.html # Integration tests (Browser)
699
1002
  ├── dist/ # Build output
700
1003
  │ ├── index.js # ESM bundle
701
1004
  │ └── types/ # TypeScript declarations
@@ -788,7 +1091,7 @@ Once published to npm, the package is automatically available on CDNs:
788
1091
  **Version pinning** (recommended for production):
789
1092
  ```html
790
1093
  <script type="module">
791
- import LicenseSeat from 'https://esm.sh/@licenseseat/js@0.3.0';
1094
+ import LicenseSeat from 'https://esm.sh/@licenseseat/js@0.4.0';
792
1095
  </script>
793
1096
  ```
794
1097
 
@@ -907,8 +1210,8 @@ This version introduces the v1 API with significant changes:
907
1210
  await sdk.getOfflineLicense(key);
908
1211
  await sdk.getPublicKey(keyId);
909
1212
 
910
- // After
911
- await sdk.getOfflineToken(key);
1213
+ // After (note: getOfflineToken uses cached license, no parameter needed)
1214
+ await sdk.getOfflineToken();
912
1215
  await sdk.getSigningKey(keyId);
913
1216
  ```
914
1217