@licenseseat/js 0.2.2 → 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
@@ -4,7 +4,7 @@
4
4
  [![npm version](https://img.shields.io/npm/v/@licenseseat/js.svg)](https://www.npmjs.com/package/@licenseseat/js)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- Official JavaScript/TypeScript SDK for [LicenseSeat](https://licenseseat.com) – the simple, secure licensing platform for apps, games, and plugins.
7
+ The official JavaScript/TypeScript SDK for [LicenseSeat](https://licenseseat.com) – the simple, secure licensing platform for apps, games, and plugins.
8
8
 
9
9
  ---
10
10
 
@@ -43,7 +43,10 @@ pnpm add @licenseseat/js
43
43
  <script type="module">
44
44
  import LicenseSeat from 'https://esm.sh/@licenseseat/js';
45
45
 
46
- const sdk = new LicenseSeat({ apiKey: 'your-api-key' });
46
+ const sdk = new LicenseSeat({
47
+ apiKey: 'your-api-key',
48
+ productSlug: 'your-product'
49
+ });
47
50
  </script>
48
51
 
49
52
  <!-- ESM via unpkg -->
@@ -69,6 +72,7 @@ import LicenseSeat from '@licenseseat/js';
69
72
  // Create SDK instance
70
73
  const sdk = new LicenseSeat({
71
74
  apiKey: 'your-api-key',
75
+ productSlug: 'your-product', // Required: Your product slug
72
76
  debug: true
73
77
  });
74
78
 
@@ -98,6 +102,7 @@ import LicenseSeat, {
98
102
 
99
103
  const config: LicenseSeatConfig = {
100
104
  apiKey: 'your-api-key',
105
+ productSlug: 'your-product',
101
106
  debug: true
102
107
  };
103
108
 
@@ -117,11 +122,14 @@ TypeScript users get full type support automatically – the package includes ge
117
122
 
118
123
  ```javascript
119
124
  const sdk = new LicenseSeat({
125
+ // Required
126
+ productSlug: 'your-product', // Your product slug from LicenseSeat dashboard
127
+
120
128
  // Required for authenticated operations
121
129
  apiKey: 'your-api-key',
122
130
 
123
131
  // API Configuration
124
- apiBaseUrl: 'https://licenseseat.com/api', // Default
132
+ apiBaseUrl: 'https://licenseseat.com/api/v1', // Default
125
133
 
126
134
  // Storage
127
135
  storagePrefix: 'licenseseat_', // localStorage key prefix
@@ -148,18 +156,19 @@ const sdk = new LicenseSeat({
148
156
 
149
157
  ### Configuration Options
150
158
 
151
- | Option | Type | Default | Description |
152
- | ------------------------ | --------- | ------------------------------- | --------------------------------------------------------- |
153
- | `apiKey` | `string` | `null` | API key for authentication (required for most operations) |
154
- | `apiBaseUrl` | `string` | `'https://licenseseat.com/api'` | API base URL |
155
- | `storagePrefix` | `string` | `'licenseseat_'` | Prefix for localStorage keys |
156
- | `autoValidateInterval` | `number` | `3600000` | Auto-validation interval in ms (1 hour) |
157
- | `autoInitialize` | `boolean` | `true` | Auto-initialize and validate cached license |
158
- | `offlineFallbackEnabled` | `boolean` | `false` | Enable offline validation on network errors |
159
- | `maxOfflineDays` | `number` | `0` | Maximum days license works offline (0 = disabled) |
160
- | `maxRetries` | `number` | `3` | Max retry attempts for failed API calls |
161
- | `retryDelay` | `number` | `1000` | Initial retry delay in ms (exponential backoff) |
162
- | `debug` | `boolean` | `false` | Enable debug logging to console |
159
+ | Option | Type | Default | Description |
160
+ | ------------------------ | --------- | ---------------------------------- | --------------------------------------------------------- |
161
+ | `productSlug` | `string` | | **Required.** Your product slug from the dashboard |
162
+ | `apiKey` | `string` | `null` | API key for authentication (required for most operations) |
163
+ | `apiBaseUrl` | `string` | `'https://licenseseat.com/api/v1'` | API base URL |
164
+ | `storagePrefix` | `string` | `'licenseseat_'` | Prefix for localStorage keys |
165
+ | `autoValidateInterval` | `number` | `3600000` | Auto-validation interval in ms (1 hour) |
166
+ | `autoInitialize` | `boolean` | `true` | Auto-initialize and validate cached license |
167
+ | `offlineFallbackEnabled` | `boolean` | `false` | Enable offline validation on network errors |
168
+ | `maxOfflineDays` | `number` | `0` | Maximum days license works offline (0 = disabled) |
169
+ | `maxRetries` | `number` | `3` | Max retry attempts for failed API calls |
170
+ | `retryDelay` | `number` | `1000` | Initial retry delay in ms (exponential backoff) |
171
+ | `debug` | `boolean` | `false` | Enable debug logging to console |
163
172
 
164
173
  ---
165
174
 
@@ -173,17 +182,24 @@ Activates a license key on this device.
173
182
 
174
183
  ```javascript
175
184
  const result = await sdk.activate('LICENSE-KEY', {
176
- deviceIdentifier: 'custom-device-id', // Optional: auto-generated if not provided
177
- softwareReleaseDate: '2024-01-15', // Optional: for version-aware licensing
178
- metadata: { version: '1.0.0' } // Optional: custom metadata
185
+ deviceId: 'custom-device-id', // Optional: auto-generated if not provided
186
+ deviceName: "John's MacBook Pro", // Optional: human-readable device name
187
+ metadata: { version: '1.0.0' } // Optional: custom metadata
179
188
  });
180
189
 
181
190
  console.log(result);
182
191
  // {
183
192
  // license_key: 'LICENSE-KEY',
184
- // device_identifier: 'web-abc123-xyz',
193
+ // device_id: 'web-abc123',
185
194
  // activated_at: '2024-01-15T10:30:00Z',
186
- // activation: { ... }
195
+ // activation: {
196
+ // object: 'activation',
197
+ // id: 123,
198
+ // device_id: 'web-abc123',
199
+ // license_key: 'LICENSE-KEY',
200
+ // activated_at: '2024-01-15T10:30:00Z',
201
+ // license: { ... }
202
+ // }
187
203
  // }
188
204
  ```
189
205
 
@@ -192,7 +208,13 @@ console.log(result);
192
208
  Deactivates the current license and clears cached data.
193
209
 
194
210
  ```javascript
195
- await sdk.deactivate();
211
+ const result = await sdk.deactivate();
212
+ console.log(result);
213
+ // {
214
+ // object: 'deactivation',
215
+ // activation_id: 123,
216
+ // deactivated_at: '2024-01-15T12:00:00Z'
217
+ // }
196
218
  ```
197
219
 
198
220
  #### `sdk.validateLicense(licenseKey, options?)`
@@ -201,25 +223,36 @@ Validates a license with the server.
201
223
 
202
224
  ```javascript
203
225
  const result = await sdk.validateLicense('LICENSE-KEY', {
204
- deviceIdentifier: 'device-id', // Optional
205
- productSlug: 'my-product' // Optional
226
+ deviceId: 'device-id' // Optional: required for hardware_locked mode
206
227
  });
207
228
 
208
229
  console.log(result);
209
230
  // {
210
231
  // valid: true,
211
- // active_entitlements: [
212
- // { key: 'pro', name: 'Pro Features', expires_at: null },
213
- // { key: 'beta', name: 'Beta Access', expires_at: '2024-12-31T23:59:59Z' }
214
- // ]
232
+ // license: {
233
+ // key: 'LICENSE-KEY',
234
+ // status: 'active',
235
+ // mode: 'hardware_locked',
236
+ // plan_key: 'pro',
237
+ // active_seats: 1,
238
+ // seat_limit: 3,
239
+ // active_entitlements: [
240
+ // { key: 'pro', expires_at: null, metadata: null },
241
+ // { key: 'beta', expires_at: '2024-12-31T23:59:59Z', metadata: null }
242
+ // ],
243
+ // product: { slug: 'your-product', name: 'Your Product' }
244
+ // },
245
+ // active_entitlements: [...]
215
246
  // }
216
247
  ```
217
248
 
218
249
  ### Entitlement Methods
219
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
+
220
253
  #### `sdk.hasEntitlement(key)`
221
254
 
222
- 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.
223
256
 
224
257
  ```javascript
225
258
  if (sdk.hasEntitlement('pro')) {
@@ -268,7 +301,7 @@ console.log(status);
268
301
  // {
269
302
  // status: 'active',
270
303
  // license: 'LICENSE-KEY',
271
- // device: 'web-abc123-xyz',
304
+ // device: 'web-abc123',
272
305
  // activated_at: '2024-01-15T10:30:00Z',
273
306
  // last_validated: '2024-01-15T11:30:00Z',
274
307
  // entitlements: [...]
@@ -277,17 +310,21 @@ console.log(status);
277
310
 
278
311
  #### `sdk.testAuth()`
279
312
 
280
- Test API authentication (useful for verifying API key).
313
+ Test API connectivity by calling the `/health` endpoint. Returns health status and API version.
281
314
 
282
315
  ```javascript
283
316
  try {
284
317
  const result = await sdk.testAuth();
285
- 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'
286
321
  } catch (error) {
287
- console.error('Auth failed:', error);
322
+ console.error('Connection failed:', error);
288
323
  }
289
324
  ```
290
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
+
291
328
  #### `sdk.reset()`
292
329
 
293
330
  Clear all cached data and reset SDK state.
@@ -312,6 +349,7 @@ Manually initialize the SDK (only needed if `autoInitialize: false`).
312
349
  ```javascript
313
350
  const sdk = new LicenseSeat({
314
351
  apiKey: 'key',
352
+ productSlug: 'your-product',
315
353
  autoInitialize: false // Don't auto-initialize
316
354
  });
317
355
 
@@ -352,7 +390,7 @@ sdk.off('activation:success', handler);
352
390
  | `activation:error` | Activation failed | `{ licenseKey, error }` |
353
391
  | **Deactivation** | | |
354
392
  | `deactivation:start` | Deactivation started | `CachedLicense` |
355
- | `deactivation:success` | Deactivation succeeded | `Object` |
393
+ | `deactivation:success` | Deactivation succeeded | `DeactivationResponse` |
356
394
  | `deactivation:error` | Deactivation failed | `{ error, license }` |
357
395
  | **Validation** | | |
358
396
  | `validation:start` | Validation started | `{ licenseKey }` |
@@ -368,13 +406,13 @@ sdk.off('activation:success', handler);
368
406
  | **Network** | | |
369
407
  | `network:online` | Network connectivity restored | – |
370
408
  | `network:offline` | Network connectivity lost | `{ error }` |
371
- | **Offline License** | | |
372
- | `offlineLicense:fetching` | Fetching offline license | `{ licenseKey }` |
373
- | `offlineLicense:fetched` | Offline license fetched | `{ licenseKey, data }` |
374
- | `offlineLicense:fetchError` | Offline license fetch failed | `{ licenseKey, error }` |
375
- | `offlineLicense:ready` | Offline assets synced | `{ kid, exp_at }` |
376
- | `offlineLicense:verified` | Offline signature verified | `{ payload }` |
377
- | `offlineLicense:verificationFailed` | Offline signature invalid | `{ payload }` |
409
+ | **Offline Token** | | |
410
+ | `offlineToken:fetching` | Fetching offline token | `{ licenseKey }` |
411
+ | `offlineToken:fetched` | Offline token fetched | `{ licenseKey, data }` |
412
+ | `offlineToken:fetchError` | Offline token fetch failed | `{ licenseKey, error }` |
413
+ | `offlineToken:ready` | Offline assets synced | `{ kid, exp_at }` |
414
+ | `offlineToken:verified` | Offline signature verified | `{ payload }` |
415
+ | `offlineToken:verificationFailed` | Offline signature invalid | `{ payload }` |
378
416
 
379
417
  ---
380
418
 
@@ -386,7 +424,10 @@ For applications that need a shared SDK instance:
386
424
  import { configure, getSharedInstance, resetSharedInstance } from '@licenseseat/js';
387
425
 
388
426
  // Configure once at app startup
389
- configure({ apiKey: 'your-key' });
427
+ configure({
428
+ apiKey: 'your-key',
429
+ productSlug: 'your-product'
430
+ });
390
431
 
391
432
  // Use anywhere in your app
392
433
  const sdk = getSharedInstance();
@@ -400,11 +441,12 @@ resetSharedInstance();
400
441
 
401
442
  ## Offline Support
402
443
 
403
- The SDK supports offline license validation using cryptographically signed offline licenses (Ed25519).
444
+ The SDK supports offline license validation using cryptographically signed offline tokens (Ed25519).
404
445
 
405
446
  ```javascript
406
447
  const sdk = new LicenseSeat({
407
448
  apiKey: 'your-key',
449
+ productSlug: 'your-product',
408
450
  offlineFallbackEnabled: true, // Enable offline fallback
409
451
  maxOfflineDays: 7 // Allow 7 days offline
410
452
  });
@@ -421,11 +463,109 @@ if (result.offline) {
421
463
 
422
464
  ### How Offline Validation Works
423
465
 
424
- 1. On activation, the SDK fetches a signed offline license from the server
425
- 2. The offline license contains the license data + Ed25519 signature
466
+ 1. On activation, the SDK fetches a signed offline token from the server
467
+ 2. The offline token contains:
468
+ - License data (key, plan, entitlements, expiration)
469
+ - Ed25519 signature
470
+ - Canonical JSON for verification
426
471
  3. When offline, the SDK verifies the signature locally
427
472
  4. Clock tamper detection prevents users from bypassing expiration
428
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
+
538
+ ### Offline Token Structure
539
+
540
+ ```javascript
541
+ {
542
+ object: 'offline_token',
543
+ token: {
544
+ schema_version: 1,
545
+ license_key: 'LICENSE-KEY',
546
+ product_slug: 'your-product',
547
+ plan_key: 'pro',
548
+ mode: 'hardware_locked',
549
+ device_id: 'web-abc123',
550
+ iat: 1704067200, // Issued at (Unix timestamp)
551
+ exp: 1706659200, // Expires at (Unix timestamp)
552
+ nbf: 1704067200, // Not before (Unix timestamp)
553
+ license_expires_at: null,
554
+ kid: 'key-id-001',
555
+ entitlements: [
556
+ { key: 'pro', expires_at: null }
557
+ ],
558
+ metadata: {}
559
+ },
560
+ signature: {
561
+ algorithm: 'Ed25519',
562
+ key_id: 'key-id-001',
563
+ value: 'base64url-encoded-signature'
564
+ },
565
+ canonical: '{"entitlements":[...],"exp":...}'
566
+ }
567
+ ```
568
+
429
569
  ---
430
570
 
431
571
  ## Error Handling
@@ -445,9 +585,12 @@ try {
445
585
  } catch (error) {
446
586
  if (error instanceof APIError) {
447
587
  console.log('HTTP Status:', error.status);
448
- console.log('Response:', error.data);
588
+ console.log('Error Code:', error.data?.error?.code);
589
+ console.log('Error Message:', error.data?.error?.message);
449
590
  } else if (error instanceof LicenseError) {
450
591
  console.log('License error:', error.code);
592
+ } else if (error instanceof ConfigurationError) {
593
+ console.log('Config error:', error.message);
451
594
  }
452
595
  }
453
596
  ```
@@ -458,16 +601,72 @@ try {
458
601
  | -------------------- | ---------------------------------------------------- |
459
602
  | `APIError` | HTTP request failures (includes `status` and `data`) |
460
603
  | `LicenseError` | License operation failures (includes `code`) |
461
- | `ConfigurationError` | SDK misconfiguration |
604
+ | `ConfigurationError` | SDK misconfiguration (e.g., missing `productSlug`) |
462
605
  | `CryptoError` | Cryptographic operation failures |
463
606
 
607
+ ### API Error Format
608
+
609
+ API errors follow this structure:
610
+
611
+ ```javascript
612
+ {
613
+ error: {
614
+ code: 'license_not_found', // Machine-readable error code
615
+ message: 'License not found.', // Human-readable message
616
+ details: { ... } // Optional additional details
617
+ }
618
+ }
619
+ ```
620
+
621
+ Common error codes:
622
+ - `unauthorized` - Invalid or missing API key
623
+ - `license_not_found` - License key doesn't exist
624
+ - `license_expired` - License has expired
625
+ - `license_suspended` - License is suspended
626
+ - `license_revoked` - License has been revoked
627
+ - `seat_limit_reached` - No more seats available
628
+ - `device_already_activated` - Device is already activated
629
+ - `activation_not_found` - Activation doesn't exist (for deactivation)
630
+
464
631
  ---
465
632
 
466
633
  ## Browser Support
467
634
 
468
635
  - **Modern browsers**: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+
469
636
  - **Bundlers**: Vite, Webpack, Rollup, esbuild, Parcel
470
- - **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()`.
471
670
 
472
671
  ---
473
672
 
@@ -480,7 +679,10 @@ Simply import and use:
480
679
  ```javascript
481
680
  import LicenseSeat from '@licenseseat/js';
482
681
 
483
- const sdk = new LicenseSeat({ apiKey: 'your-key' });
682
+ const sdk = new LicenseSeat({
683
+ apiKey: 'your-key',
684
+ productSlug: 'your-product'
685
+ });
484
686
  ```
485
687
 
486
688
  ### For TypeScript Users
@@ -491,7 +693,10 @@ The package includes TypeScript declarations (`.d.ts` files) automatically. No a
491
693
  import LicenseSeat from '@licenseseat/js';
492
694
 
493
695
  // Types are automatically available
494
- const sdk = new LicenseSeat({ apiKey: 'your-key' });
696
+ const sdk = new LicenseSeat({
697
+ apiKey: 'your-key',
698
+ productSlug: 'your-product'
699
+ });
495
700
 
496
701
  // Import specific types if needed
497
702
  import type {
@@ -500,7 +705,10 @@ import type {
500
705
  EntitlementCheckResult,
501
706
  LicenseStatus,
502
707
  Entitlement,
503
- CachedLicense
708
+ CachedLicense,
709
+ ActivationResponse,
710
+ DeactivationResponse,
711
+ OfflineToken
504
712
  } from '@licenseseat/js';
505
713
  ```
506
714
 
@@ -520,6 +728,7 @@ Use ES modules via CDN:
520
728
 
521
729
  const sdk = new LicenseSeat({
522
730
  apiKey: 'your-api-key',
731
+ productSlug: 'your-product',
523
732
  debug: true
524
733
  });
525
734
 
@@ -573,6 +782,50 @@ npm install
573
782
  | `npm run test:coverage` | Run tests with coverage report |
574
783
  | `npm run typecheck` | Type-check without emitting |
575
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
+
576
829
  ### Project Structure
577
830
 
578
831
  ```
@@ -584,11 +837,13 @@ licenseseat-js/
584
837
  │ ├── errors.js # Error classes
585
838
  │ ├── types.js # JSDoc type definitions
586
839
  │ └── utils.js # Utility functions
587
- ├── tests/
840
+ ├── tests/ # Unit tests (mocked API)
588
841
  │ ├── setup.js # Test setup
589
842
  │ ├── mocks/ # MSW handlers
590
843
  │ ├── LicenseSeat.test.js
591
844
  │ └── utils.test.js
845
+ ├── test-live.mjs # Integration tests (Node.js)
846
+ ├── test-live.html # Integration tests (Browser)
592
847
  ├── dist/ # Build output
593
848
  │ ├── index.js # ESM bundle
594
849
  │ └── types/ # TypeScript declarations
@@ -681,7 +936,7 @@ Once published to npm, the package is automatically available on CDNs:
681
936
  **Version pinning** (recommended for production):
682
937
  ```html
683
938
  <script type="module">
684
- import LicenseSeat from 'https://esm.sh/@licenseseat/js@0.2.0';
939
+ import LicenseSeat from 'https://esm.sh/@licenseseat/js@0.3.0';
685
940
  </script>
686
941
  ```
687
942
 
@@ -711,7 +966,10 @@ This creates `dist/index.global.js`:
711
966
  ```html
712
967
  <script src="/path/to/index.global.js"></script>
713
968
  <script>
714
- const sdk = new LicenseSeat({ apiKey: 'your-key' });
969
+ const sdk = new LicenseSeat({
970
+ apiKey: 'your-key',
971
+ productSlug: 'your-product'
972
+ });
715
973
  </script>
716
974
  ```
717
975
 
@@ -727,22 +985,80 @@ This project follows [Semantic Versioning](https://semver.org/):
727
985
 
728
986
  ---
729
987
 
730
- ## Migration from v0.1.x
988
+ ## Migration from v0.2.x
989
+
990
+ ### Breaking Changes in v0.3.0
991
+
992
+ This version introduces the v1 API with significant changes:
993
+
994
+ | Change | Before (v0.2.x) | After (v0.3.0) |
995
+ | -------------------------------- | ------------------------------- | ----------------------------------------- |
996
+ | `productSlug` config | Not required | **Required** for all API operations |
997
+ | `apiBaseUrl` default | `https://licenseseat.com/api` | `https://licenseseat.com/api/v1` |
998
+ | `deviceIdentifier` option | `deviceIdentifier` | `deviceId` |
999
+ | `device_identifier` field | `device_identifier` | `device_id` |
1000
+ | Deactivation response | Returns full activation object | Returns `{ object, activation_id, deactivated_at }` |
1001
+ | `getOfflineLicense()` method | Available | Renamed to `getOfflineToken()` |
1002
+ | `getPublicKey()` method | Available | Renamed to `getSigningKey()` |
1003
+ | Offline license structure | Legacy format | New token/signature/canonical format |
1004
+ | Error format | Various | `{ error: { code, message, details? } }` |
1005
+
1006
+ ### Migration Steps
1007
+
1008
+ 1. **Add `productSlug` to configuration:**
1009
+ ```javascript
1010
+ // Before
1011
+ const sdk = new LicenseSeat({ apiKey: 'key' });
1012
+
1013
+ // After
1014
+ const sdk = new LicenseSeat({
1015
+ apiKey: 'key',
1016
+ productSlug: 'your-product' // Required!
1017
+ });
1018
+ ```
1019
+
1020
+ 2. **Update activation options:**
1021
+ ```javascript
1022
+ // Before
1023
+ await sdk.activate('KEY', { deviceIdentifier: 'id' });
731
1024
 
732
- ### Breaking Changes in v0.2.0
1025
+ // After
1026
+ await sdk.activate('KEY', { deviceId: 'id' });
1027
+ ```
733
1028
 
734
- | Change | Before | After | Migration |
735
- | -------------------------------- | ------ | ----------------------------- | --------------------------------------------------------------- |
736
- | `apiBaseUrl` default | `/api` | `https://licenseseat.com/api` | Set `apiBaseUrl` explicitly if using a relative URL |
737
- | `offlineFallbackEnabled` default | `true` | `false` | Set `offlineFallbackEnabled: true` if you need offline fallback |
1029
+ 3. **Update response field access:**
1030
+ ```javascript
1031
+ // Before
1032
+ const result = await sdk.activate('KEY');
1033
+ console.log(result.device_identifier);
738
1034
 
739
- ### New Features in v0.2.0
1035
+ // After
1036
+ const result = await sdk.activate('KEY');
1037
+ console.log(result.device_id);
1038
+ ```
1039
+
1040
+ 4. **Update deactivation handling:**
1041
+ ```javascript
1042
+ // Before
1043
+ const result = await sdk.deactivate();
1044
+ console.log(result.license_key);
1045
+
1046
+ // After
1047
+ const result = await sdk.deactivate();
1048
+ console.log(result.activation_id);
1049
+ console.log(result.deactivated_at);
1050
+ ```
740
1051
 
741
- - `hasEntitlement(key)` method for simple boolean checks
742
- - `autoInitialize` config option for lazy initialization
743
- - Full TypeScript support with auto-generated `.d.ts` files
744
- - Singleton pattern with `configure()` and `getSharedInstance()`
745
- - New error classes: `LicenseError`, `ConfigurationError`, `CryptoError`
1052
+ 5. **Update offline method calls:**
1053
+ ```javascript
1054
+ // Before
1055
+ await sdk.getOfflineLicense(key);
1056
+ await sdk.getPublicKey(keyId);
1057
+
1058
+ // After (note: getOfflineToken uses cached license, no parameter needed)
1059
+ await sdk.getOfflineToken();
1060
+ await sdk.getSigningKey(keyId);
1061
+ ```
746
1062
 
747
1063
  ---
748
1064
 
@@ -760,10 +1076,3 @@ MIT License – see [LICENSE](LICENSE) for details.
760
1076
  - [GitHub Repository](https://github.com/licenseseat/licenseseat-js)
761
1077
  - [npm Package](https://www.npmjs.com/package/@licenseseat/js)
762
1078
  - [Report Issues](https://github.com/licenseseat/licenseseat-js/issues)
763
-
764
- ---
765
-
766
- ## Support
767
-
768
- - **Email**: support@licenseseat.com
769
- - **GitHub Issues**: [Report a bug](https://github.com/licenseseat/licenseseat-js/issues/new)