@licenseseat/js 0.3.0 → 0.3.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
@@ -248,9 +248,11 @@ console.log(result);
248
248
 
249
249
  ### Entitlement Methods
250
250
 
251
+ > **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.
252
+
251
253
  #### `sdk.hasEntitlement(key)`
252
254
 
253
- Check if an entitlement is active. Returns a simple boolean.
255
+ Check if an entitlement is active. Returns a simple boolean. Returns `false` if no entitlements exist.
254
256
 
255
257
  ```javascript
256
258
  if (sdk.hasEntitlement('pro')) {
@@ -308,17 +310,21 @@ console.log(status);
308
310
 
309
311
  #### `sdk.testAuth()`
310
312
 
311
- Test API authentication (useful for verifying API key).
313
+ Test API connectivity by calling the `/health` endpoint. Returns health status and API version.
312
314
 
313
315
  ```javascript
314
316
  try {
315
317
  const result = await sdk.testAuth();
316
- console.log('Authenticated:', result.authenticated);
318
+ console.log('Authenticated:', result.authenticated); // Always true if request succeeds
319
+ console.log('Healthy:', result.healthy); // API health status
320
+ console.log('API Version:', result.api_version); // e.g., '1.0.0'
317
321
  } catch (error) {
318
- console.error('Auth failed:', error);
322
+ console.error('Connection failed:', error);
319
323
  }
320
324
  ```
321
325
 
326
+ > **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()`.
327
+
322
328
  #### `sdk.reset()`
323
329
 
324
330
  Clear all cached data and reset SDK state.
@@ -465,6 +471,70 @@ if (result.offline) {
465
471
  3. When offline, the SDK verifies the signature locally
466
472
  4. Clock tamper detection prevents users from bypassing expiration
467
473
 
474
+ ### Offline Methods
475
+
476
+ #### `sdk.syncOfflineAssets()`
477
+
478
+ Fetches the offline token and signing key from the server. Uses the currently cached license. Call this after activation to prepare for offline usage.
479
+
480
+ ```javascript
481
+ // First activate (caches the license)
482
+ await sdk.activate('LICENSE-KEY');
483
+
484
+ // Then sync offline assets (uses cached license)
485
+ const assets = await sdk.syncOfflineAssets();
486
+ console.log('Offline token key ID:', assets.kid);
487
+ console.log('Expires at:', assets.exp_at);
488
+ ```
489
+
490
+ #### `sdk.getOfflineToken()`
491
+
492
+ Fetches a signed offline token for the currently cached license. Returns the token structure containing the license data and Ed25519 signature.
493
+
494
+ ```javascript
495
+ // Must have an active license cached first
496
+ const token = await sdk.getOfflineToken();
497
+ console.log(token);
498
+ // {
499
+ // object: 'offline_token',
500
+ // token: { license_key, product_slug, plan_key, ... },
501
+ // signature: { algorithm: 'Ed25519', key_id, value },
502
+ // canonical: '...'
503
+ // }
504
+ ```
505
+
506
+ #### `sdk.getSigningKey(keyId)`
507
+
508
+ Fetches the Ed25519 public key used for verifying offline token signatures.
509
+
510
+ ```javascript
511
+ const signingKey = await sdk.getSigningKey('key-id-001');
512
+ console.log(signingKey);
513
+ // {
514
+ // object: 'signing_key',
515
+ // kid: 'key-id-001',
516
+ // public_key: 'base64-encoded-public-key',
517
+ // algorithm: 'Ed25519',
518
+ // created_at: '2024-01-01T00:00:00Z'
519
+ // }
520
+ ```
521
+
522
+ #### `sdk.verifyOfflineToken(token, publicKeyB64)`
523
+
524
+ Verifies an offline token's Ed25519 signature locally. **Both parameters are required.**
525
+
526
+ ```javascript
527
+ // Fetch the token and signing key first
528
+ const token = await sdk.getOfflineToken();
529
+ const signingKey = await sdk.getSigningKey(token.signature.key_id);
530
+
531
+ // Verify the signature
532
+ const isValid = await sdk.verifyOfflineToken(token, signingKey.public_key);
533
+ console.log('Signature valid:', isValid);
534
+ ```
535
+
536
+ > **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.
537
+
468
538
  ### Offline Token Structure
469
539
 
470
540
  ```javascript
@@ -564,7 +634,39 @@ Common error codes:
564
634
 
565
635
  - **Modern browsers**: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+
566
636
  - **Bundlers**: Vite, Webpack, Rollup, esbuild, Parcel
567
- - **Node.js**: 18+ (requires polyfills for `localStorage`, `document`)
637
+ - **Node.js**: 18+ (requires polyfills - see below)
638
+
639
+ ### Node.js Usage
640
+
641
+ The SDK is designed for browsers but works in Node.js with polyfills. Add these before importing the SDK:
642
+
643
+ ```javascript
644
+ // Required polyfills for Node.js
645
+ const storage = {};
646
+ globalThis.localStorage = {
647
+ getItem(key) { return Object.prototype.hasOwnProperty.call(storage, key) ? storage[key] : null; },
648
+ setItem(key, value) { storage[key] = String(value); },
649
+ removeItem(key) { delete storage[key]; },
650
+ clear() { for (const key in storage) delete storage[key]; },
651
+ };
652
+
653
+ // Override Object.keys to support localStorage iteration (used by cache.getAllKeys())
654
+ const originalKeys = Object.keys;
655
+ Object.keys = function(obj) {
656
+ if (obj === globalThis.localStorage) return originalKeys(storage);
657
+ return originalKeys(obj);
658
+ };
659
+
660
+ // Device fingerprinting polyfills (provides stable fallback values)
661
+ globalThis.document = { createElement: () => ({ getContext: () => null }), querySelector: () => null };
662
+ globalThis.window = { navigator: {}, screen: {} };
663
+ globalThis.navigator = { userAgent: 'Node.js', language: 'en', hardwareConcurrency: 4 };
664
+
665
+ // Now import the SDK
666
+ const { default: LicenseSeat } = await import('@licenseseat/js');
667
+ ```
668
+
669
+ > **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
670
 
569
671
  ---
570
672
 
@@ -680,6 +782,50 @@ npm install
680
782
  | `npm run test:coverage` | Run tests with coverage report |
681
783
  | `npm run typecheck` | Type-check without emitting |
682
784
 
785
+ ### Integration Tests
786
+
787
+ 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.
788
+
789
+ #### Running Integration Tests (Node.js)
790
+
791
+ ```bash
792
+ # Set environment variables
793
+ export LICENSESEAT_API_KEY="ls_your_api_key_here"
794
+ export LICENSESEAT_PRODUCT_SLUG="your-product"
795
+ export LICENSESEAT_LICENSE_KEY="YOUR-LICENSE-KEY"
796
+
797
+ # Run the tests
798
+ node test-live.mjs
799
+ ```
800
+
801
+ Or with inline environment variables:
802
+
803
+ ```bash
804
+ LICENSESEAT_API_KEY=ls_xxx LICENSESEAT_PRODUCT_SLUG=my-app LICENSESEAT_LICENSE_KEY=XXX-XXX node test-live.mjs
805
+ ```
806
+
807
+ #### Running Integration Tests (Browser)
808
+
809
+ Open `test-live.html` in a browser. You'll be prompted to enter your credentials:
810
+
811
+ 1. **API Key** - Your LicenseSeat API key (starts with `ls_`)
812
+ 2. **Product Slug** - Your product identifier
813
+ 3. **License Key** - A valid license key for testing
814
+
815
+ Credentials are stored in `localStorage` for convenience during development.
816
+
817
+ #### What the Integration Tests Cover
818
+
819
+ | Category | Tests |
820
+ |----------|-------|
821
+ | **Initialization** | SDK setup, configuration defaults |
822
+ | **Activation** | License activation, device ID generation |
823
+ | **Validation** | Online validation, entitlement checking |
824
+ | **Deactivation** | License deactivation, cache clearing |
825
+ | **Offline Crypto** | Ed25519 signature verification, offline token fetching, tamper detection |
826
+ | **Error Handling** | Invalid licenses, missing config |
827
+ | **Singleton** | Shared instance pattern |
828
+
683
829
  ### Project Structure
684
830
 
685
831
  ```
@@ -691,11 +837,13 @@ licenseseat-js/
691
837
  │ ├── errors.js # Error classes
692
838
  │ ├── types.js # JSDoc type definitions
693
839
  │ └── utils.js # Utility functions
694
- ├── tests/
840
+ ├── tests/ # Unit tests (mocked API)
695
841
  │ ├── setup.js # Test setup
696
842
  │ ├── mocks/ # MSW handlers
697
843
  │ ├── LicenseSeat.test.js
698
844
  │ └── utils.test.js
845
+ ├── test-live.mjs # Integration tests (Node.js)
846
+ ├── test-live.html # Integration tests (Browser)
699
847
  ├── dist/ # Build output
700
848
  │ ├── index.js # ESM bundle
701
849
  │ └── types/ # TypeScript declarations
@@ -907,8 +1055,8 @@ This version introduces the v1 API with significant changes:
907
1055
  await sdk.getOfflineLicense(key);
908
1056
  await sdk.getPublicKey(keyId);
909
1057
 
910
- // After
911
- await sdk.getOfflineToken(key);
1058
+ // After (note: getOfflineToken uses cached license, no parameter needed)
1059
+ await sdk.getOfflineToken();
912
1060
  await sdk.getSigningKey(keyId);
913
1061
  ```
914
1062