@licenseseat/js 0.1.0 → 0.2.0

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/src/cache.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * LicenseSeat SDK Cache Manager
3
+ * Handles persistent storage of license data, offline licenses, and public keys.
4
+ * @module cache
5
+ */
6
+
7
+ /**
8
+ * License Cache Manager
9
+ * Manages persistent storage of license data using localStorage.
10
+ */
11
+ export class LicenseCache {
12
+ /**
13
+ * Create a LicenseCache instance
14
+ * @param {string} [prefix="licenseseat_"] - Prefix for all localStorage keys
15
+ */
16
+ constructor(prefix = "licenseseat_") {
17
+ /** @type {string} */
18
+ this.prefix = prefix;
19
+ /** @type {string} */
20
+ this.publicKeyCacheKey = this.prefix + "public_keys";
21
+ }
22
+
23
+ /**
24
+ * Get the cached license data
25
+ * @returns {import('./types.js').CachedLicense|null} Cached license or null if not found
26
+ */
27
+ getLicense() {
28
+ try {
29
+ const data = localStorage.getItem(this.prefix + "license");
30
+ return data ? JSON.parse(data) : null;
31
+ } catch (e) {
32
+ console.error("Failed to read license cache:", e);
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Store license data in cache
39
+ * @param {import('./types.js').CachedLicense} data - License data to cache
40
+ * @returns {void}
41
+ */
42
+ setLicense(data) {
43
+ try {
44
+ localStorage.setItem(this.prefix + "license", JSON.stringify(data));
45
+ } catch (e) {
46
+ console.error("Failed to cache license:", e);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Update the validation data for the cached license
52
+ * @param {import('./types.js').ValidationResult} validationData - Validation result to store
53
+ * @returns {void}
54
+ */
55
+ updateValidation(validationData) {
56
+ const license = this.getLicense();
57
+ if (license) {
58
+ license.validation = validationData;
59
+ license.last_validated = new Date().toISOString();
60
+ this.setLicense(license);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get the device identifier from the cached license
66
+ * @returns {string|null} Device identifier or null if not found
67
+ */
68
+ getDeviceId() {
69
+ const license = this.getLicense();
70
+ return license ? license.device_identifier : null;
71
+ }
72
+
73
+ /**
74
+ * Clear the cached license data
75
+ * @returns {void}
76
+ */
77
+ clearLicense() {
78
+ localStorage.removeItem(this.prefix + "license");
79
+ }
80
+
81
+ /**
82
+ * Get the cached offline license
83
+ * @returns {import('./types.js').SignedOfflineLicense|null} Offline license or null if not found
84
+ */
85
+ getOfflineLicense() {
86
+ try {
87
+ const data = localStorage.getItem(this.prefix + "offline_license");
88
+ return data ? JSON.parse(data) : null;
89
+ } catch (e) {
90
+ console.error("Failed to read offline license cache:", e);
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Store offline license data in cache
97
+ * @param {import('./types.js').SignedOfflineLicense} data - Signed offline license to cache
98
+ * @returns {void}
99
+ */
100
+ setOfflineLicense(data) {
101
+ try {
102
+ localStorage.setItem(
103
+ this.prefix + "offline_license",
104
+ JSON.stringify(data)
105
+ );
106
+ } catch (e) {
107
+ console.error("Failed to cache offline license:", e);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Clear the cached offline license
113
+ * @returns {void}
114
+ */
115
+ clearOfflineLicense() {
116
+ localStorage.removeItem(this.prefix + "offline_license");
117
+ }
118
+
119
+ /**
120
+ * Get a cached public key by key ID
121
+ * @param {string} keyId - The key ID to look up
122
+ * @returns {string|null} Base64-encoded public key or null if not found
123
+ */
124
+ getPublicKey(keyId) {
125
+ try {
126
+ const cache = JSON.parse(
127
+ localStorage.getItem(this.publicKeyCacheKey) || "{}"
128
+ );
129
+ return cache[keyId] || null;
130
+ } catch (e) {
131
+ console.error("Failed to read public key cache:", e);
132
+ return null;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Store a public key in cache
138
+ * @param {string} keyId - The key ID
139
+ * @param {string} publicKeyB64 - Base64-encoded public key
140
+ * @returns {void}
141
+ */
142
+ setPublicKey(keyId, publicKeyB64) {
143
+ try {
144
+ const cache = JSON.parse(
145
+ localStorage.getItem(this.publicKeyCacheKey) || "{}"
146
+ );
147
+ cache[keyId] = publicKeyB64;
148
+ localStorage.setItem(this.publicKeyCacheKey, JSON.stringify(cache));
149
+ } catch (e) {
150
+ console.error("Failed to cache public key:", e);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Clear all LicenseSeat SDK data for this prefix
156
+ * @returns {void}
157
+ */
158
+ clear() {
159
+ Object.keys(localStorage).forEach((key) => {
160
+ if (key.startsWith(this.prefix)) {
161
+ localStorage.removeItem(key);
162
+ }
163
+ });
164
+ this.clearOfflineLicense();
165
+ localStorage.removeItem(this.prefix + "last_seen_ts");
166
+ }
167
+
168
+ /**
169
+ * Get the last seen timestamp (for clock tamper detection)
170
+ * @returns {number|null} Unix timestamp in milliseconds or null if not set
171
+ */
172
+ getLastSeenTimestamp() {
173
+ const v = localStorage.getItem(this.prefix + "last_seen_ts");
174
+ return v ? parseInt(v, 10) : null;
175
+ }
176
+
177
+ /**
178
+ * Store the last seen timestamp
179
+ * @param {number} ts - Unix timestamp in milliseconds
180
+ * @returns {void}
181
+ */
182
+ setLastSeenTimestamp(ts) {
183
+ try {
184
+ localStorage.setItem(this.prefix + "last_seen_ts", String(ts));
185
+ } catch (e) {
186
+ // Ignore storage errors for timestamp
187
+ }
188
+ }
189
+ }
package/src/errors.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * LicenseSeat SDK Error Classes
3
+ * @module errors
4
+ */
5
+
6
+ /**
7
+ * Custom API Error class for HTTP request failures
8
+ * @extends Error
9
+ */
10
+ export class APIError extends Error {
11
+ /**
12
+ * Create an APIError
13
+ * @param {string} message - Error message
14
+ * @param {number} status - HTTP status code (0 for network failures)
15
+ * @param {import('./types.js').APIErrorData} [data] - Additional error data from the API response
16
+ */
17
+ constructor(message, status, data) {
18
+ super(message);
19
+ /** @type {string} */
20
+ this.name = "APIError";
21
+ /** @type {number} */
22
+ this.status = status;
23
+ /** @type {import('./types.js').APIErrorData|undefined} */
24
+ this.data = data;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Error thrown when SDK operations are attempted without proper configuration
30
+ * @extends Error
31
+ */
32
+ export class ConfigurationError extends Error {
33
+ /**
34
+ * Create a ConfigurationError
35
+ * @param {string} message - Error message
36
+ */
37
+ constructor(message) {
38
+ super(message);
39
+ /** @type {string} */
40
+ this.name = "ConfigurationError";
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Error thrown when license operations fail
46
+ * @extends Error
47
+ */
48
+ export class LicenseError extends Error {
49
+ /**
50
+ * Create a LicenseError
51
+ * @param {string} message - Error message
52
+ * @param {string} [code] - Machine-readable error code
53
+ */
54
+ constructor(message, code) {
55
+ super(message);
56
+ /** @type {string} */
57
+ this.name = "LicenseError";
58
+ /** @type {string|undefined} */
59
+ this.code = code;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Error thrown when cryptographic operations fail
65
+ * @extends Error
66
+ */
67
+ export class CryptoError extends Error {
68
+ /**
69
+ * Create a CryptoError
70
+ * @param {string} message - Error message
71
+ */
72
+ constructor(message) {
73
+ super(message);
74
+ /** @type {string} */
75
+ this.name = "CryptoError";
76
+ }
77
+ }
package/src/index.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * LicenseSeat JavaScript SDK
3
+ *
4
+ * Official JavaScript client for LicenseSeat - the simple, secure licensing platform
5
+ * for apps, games, and plugins.
6
+ *
7
+ * @module @licenseseat/js
8
+ * @version 0.2.0
9
+ *
10
+ * @example
11
+ * ```js
12
+ * import LicenseSeat from '@licenseseat/js';
13
+ *
14
+ * const sdk = new LicenseSeat({
15
+ * apiKey: 'your-api-key',
16
+ * debug: true
17
+ * });
18
+ *
19
+ * // Activate a license
20
+ * await sdk.activate('LICENSE-KEY-HERE');
21
+ *
22
+ * // Check entitlements
23
+ * if (sdk.hasEntitlement('pro')) {
24
+ * // Enable pro features
25
+ * }
26
+ *
27
+ * // Get license status
28
+ * const status = sdk.getStatus();
29
+ * console.log(status);
30
+ * ```
31
+ */
32
+
33
+ // Re-export the main SDK class
34
+ export {
35
+ LicenseSeatSDK,
36
+ getSharedInstance,
37
+ configure,
38
+ resetSharedInstance,
39
+ } from "./LicenseSeat.js";
40
+
41
+ // Re-export types (empty module, but useful for documentation)
42
+ export {} from "./types.js";
43
+
44
+ // Re-export error classes
45
+ export { APIError, ConfigurationError, LicenseError, CryptoError } from "./errors.js";
46
+
47
+ // Re-export cache (for advanced use cases)
48
+ export { LicenseCache } from "./cache.js";
49
+
50
+ // Re-export utility functions (for advanced use cases)
51
+ export {
52
+ parseActiveEntitlements,
53
+ constantTimeEqual,
54
+ canonicalJsonStringify,
55
+ base64UrlDecode,
56
+ generateDeviceId,
57
+ getCsrfToken,
58
+ } from "./utils.js";
59
+
60
+ // Default export - the main SDK class
61
+ import { LicenseSeatSDK } from "./LicenseSeat.js";
62
+ export default LicenseSeatSDK;
package/src/types.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * LicenseSeat SDK Type Definitions
3
+ * These JSDoc types enable TypeScript support via declaration file generation.
4
+ * @module types
5
+ */
6
+
7
+ /**
8
+ * SDK Configuration options
9
+ * @typedef {Object} LicenseSeatConfig
10
+ * @property {string} [apiBaseUrl="https://api.licenseseat.com"] - Base URL for the LicenseSeat API
11
+ * @property {string} [apiKey] - API key for authentication (required for most operations)
12
+ * @property {string} [storagePrefix="licenseseat_"] - Prefix for localStorage keys
13
+ * @property {number} [autoValidateInterval=3600000] - Interval in ms for automatic license validation (default: 1 hour)
14
+ * @property {number} [networkRecheckInterval=30000] - Interval in ms to check network connectivity when offline (default: 30s)
15
+ * @property {number} [maxRetries=3] - Maximum number of retry attempts for failed API calls
16
+ * @property {number} [retryDelay=1000] - Initial delay in ms between retries (exponential backoff applied)
17
+ * @property {boolean} [debug=false] - Enable debug logging to console
18
+ * @property {number} [offlineLicenseRefreshInterval=259200000] - Interval in ms to refresh offline license (default: 72 hours)
19
+ * @property {boolean} [offlineFallbackEnabled=false] - Enable offline validation fallback on network errors
20
+ * @property {number} [maxOfflineDays=0] - Maximum days a license can be used offline (0 = disabled)
21
+ * @property {number} [maxClockSkewMs=300000] - Maximum allowed clock skew in ms for offline validation (default: 5 minutes)
22
+ * @property {boolean} [autoInitialize=true] - Automatically initialize and validate cached license on construction
23
+ */
24
+
25
+ /**
26
+ * License activation options
27
+ * @typedef {Object} ActivationOptions
28
+ * @property {string} [deviceIdentifier] - Custom device identifier (auto-generated if not provided)
29
+ * @property {string} [softwareReleaseDate] - ISO8601 date string for version-aware activation
30
+ * @property {Object} [metadata] - Additional metadata to include with the activation
31
+ */
32
+
33
+ /**
34
+ * License validation options
35
+ * @typedef {Object} ValidationOptions
36
+ * @property {string} [deviceIdentifier] - Device identifier to validate against
37
+ * @property {string} [productSlug] - Product slug for product-specific validation
38
+ */
39
+
40
+ /**
41
+ * Activation response from the API
42
+ * @typedef {Object} ActivationResponse
43
+ * @property {string} id - Activation ID
44
+ * @property {string} license_key - The activated license key
45
+ * @property {string} device_identifier - Device identifier used for activation
46
+ * @property {string} activated_at - ISO8601 timestamp of activation
47
+ * @property {Object} [metadata] - Additional metadata
48
+ */
49
+
50
+ /**
51
+ * Cached license data
52
+ * @typedef {Object} CachedLicense
53
+ * @property {string} license_key - The license key
54
+ * @property {string} device_identifier - Device identifier
55
+ * @property {ActivationResponse} [activation] - Original activation response
56
+ * @property {string} activated_at - ISO8601 timestamp of activation
57
+ * @property {string} last_validated - ISO8601 timestamp of last validation
58
+ * @property {ValidationResult} [validation] - Latest validation result
59
+ */
60
+
61
+ /**
62
+ * Entitlement object
63
+ * @typedef {Object} Entitlement
64
+ * @property {string} key - Unique entitlement key
65
+ * @property {string|null} name - Human-readable name
66
+ * @property {string|null} description - Description of the entitlement
67
+ * @property {string|null} expires_at - ISO8601 expiration timestamp
68
+ * @property {Object|null} metadata - Additional metadata
69
+ */
70
+
71
+ /**
72
+ * Validation result from API or offline verification
73
+ * @typedef {Object} ValidationResult
74
+ * @property {boolean} valid - Whether the license is valid
75
+ * @property {boolean} [offline] - Whether this was an offline validation
76
+ * @property {string} [reason] - Reason for invalid status (online)
77
+ * @property {string} [reason_code] - Machine-readable reason code (offline)
78
+ * @property {Entitlement[]} [active_entitlements] - List of active entitlements
79
+ * @property {boolean} [optimistic] - Whether this is an optimistic validation (pending server confirmation)
80
+ */
81
+
82
+ /**
83
+ * Entitlement check result
84
+ * @typedef {Object} EntitlementCheckResult
85
+ * @property {boolean} active - Whether the entitlement is active
86
+ * @property {string} [reason] - Reason if not active ("no_license" | "not_found" | "expired")
87
+ * @property {string} [expires_at] - ISO8601 expiration timestamp if expired
88
+ * @property {Entitlement} [entitlement] - Full entitlement object if active
89
+ */
90
+
91
+ /**
92
+ * License status object
93
+ * @typedef {Object} LicenseStatus
94
+ * @property {string} status - Status string ("inactive" | "pending" | "invalid" | "offline-invalid" | "offline-valid" | "active")
95
+ * @property {string} [message] - Human-readable status message
96
+ * @property {string} [license] - License key (if active)
97
+ * @property {string} [device] - Device identifier (if active)
98
+ * @property {string} [activated_at] - ISO8601 activation timestamp
99
+ * @property {string} [last_validated] - ISO8601 last validation timestamp
100
+ * @property {Entitlement[]} [entitlements] - List of active entitlements
101
+ */
102
+
103
+ /**
104
+ * Offline license payload
105
+ * @typedef {Object} OfflineLicensePayload
106
+ * @property {string} [lic_k] - License key
107
+ * @property {string} [exp_at] - ISO8601 expiration timestamp
108
+ * @property {string} [kid] - Key ID for signature verification
109
+ * @property {Array<Object>} [active_ents] - Active entitlements
110
+ * @property {Array<Object>} [active_entitlements] - Active entitlements (alternative key)
111
+ * @property {Object} [metadata] - Additional metadata
112
+ */
113
+
114
+ /**
115
+ * Signed offline license data
116
+ * @typedef {Object} SignedOfflineLicense
117
+ * @property {OfflineLicensePayload} payload - The license payload
118
+ * @property {string} signature_b64u - Base64URL-encoded Ed25519 signature
119
+ * @property {string} [kid] - Key ID for public key lookup
120
+ */
121
+
122
+ /**
123
+ * Event callback function
124
+ * @callback EventCallback
125
+ * @param {*} data - Event data
126
+ * @returns {void}
127
+ */
128
+
129
+ /**
130
+ * Event unsubscribe function
131
+ * @callback EventUnsubscribe
132
+ * @returns {void}
133
+ */
134
+
135
+ /**
136
+ * API Error data
137
+ * @typedef {Object} APIErrorData
138
+ * @property {string} [error] - Error message
139
+ * @property {string} [code] - Error code
140
+ * @property {Object} [details] - Additional error details
141
+ */
142
+
143
+ // Export empty object to make this a module
144
+ export {};
package/src/utils.js ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * LicenseSeat SDK Utility Functions
3
+ * @module utils
4
+ */
5
+
6
+ // @ts-ignore - canonical-json has a default export that is the stringify function
7
+ import CJSON from "canonical-json";
8
+
9
+ /**
10
+ * Parse active entitlements from a raw API payload into a consistent shape
11
+ * @param {Object} [payload={}] - Raw payload from API or offline license
12
+ * @returns {import('./types.js').Entitlement[]} Normalized entitlements array
13
+ */
14
+ export function parseActiveEntitlements(payload = {}) {
15
+ const raw = payload.active_ents || payload.active_entitlements || [];
16
+ return raw.map((e) => ({
17
+ key: e.key,
18
+ name: e.name ?? null,
19
+ description: e.description ?? null,
20
+ expires_at: e.expires_at ?? null,
21
+ metadata: e.metadata ?? null,
22
+ }));
23
+ }
24
+
25
+ /**
26
+ * Constant-time string comparison to mitigate timing attacks
27
+ * @param {string} [a=""] - First string
28
+ * @param {string} [b=""] - Second string
29
+ * @returns {boolean} True if strings are equal
30
+ */
31
+ export function constantTimeEqual(a = "", b = "") {
32
+ if (a.length !== b.length) return false;
33
+ let res = 0;
34
+ for (let i = 0; i < a.length; i++) {
35
+ res |= a.charCodeAt(i) ^ b.charCodeAt(i);
36
+ }
37
+ return res === 0;
38
+ }
39
+
40
+ /**
41
+ * Generate a canonical JSON string from an object (keys sorted)
42
+ * This is crucial for consistent signature verification.
43
+ * @param {Object} obj - The object to stringify
44
+ * @returns {string} Canonical JSON string
45
+ */
46
+ export function canonicalJsonStringify(obj) {
47
+ // canonical-json exports the stringify function directly as default
48
+ /** @type {Function|undefined} */
49
+ const stringify = typeof CJSON === "function" ? CJSON : (CJSON && typeof CJSON === "object" ? /** @type {any} */ (CJSON).stringify : undefined);
50
+ if (!stringify || typeof stringify !== "function") {
51
+ console.warn(
52
+ "[LicenseSeat SDK] canonical-json library not loaded correctly. Falling back to basic JSON.stringify. Signature verification might be unreliable if server uses different canonicalization."
53
+ );
54
+ try {
55
+ const sortedObj = {};
56
+ Object.keys(obj)
57
+ .sort()
58
+ .forEach((key) => {
59
+ sortedObj[key] = obj[key];
60
+ });
61
+ return JSON.stringify(sortedObj);
62
+ } catch (e) {
63
+ return JSON.stringify(obj);
64
+ }
65
+ }
66
+ return stringify(obj);
67
+ }
68
+
69
+ /**
70
+ * Decode a Base64URL string to a Uint8Array
71
+ * @param {string} base64UrlString - The Base64URL encoded string
72
+ * @returns {Uint8Array} Decoded bytes
73
+ */
74
+ export function base64UrlDecode(base64UrlString) {
75
+ let base64 = base64UrlString.replace(/-/g, "+").replace(/_/g, "/");
76
+ while (base64.length % 4) {
77
+ base64 += "=";
78
+ }
79
+ const raw = window.atob(base64);
80
+ const outputArray = new Uint8Array(raw.length);
81
+ for (let i = 0; i < raw.length; ++i) {
82
+ outputArray[i] = raw.charCodeAt(i);
83
+ }
84
+ return outputArray;
85
+ }
86
+
87
+ /**
88
+ * Simple hash function for generating device fingerprints
89
+ * @param {string} str - String to hash
90
+ * @returns {string} Base36 encoded hash
91
+ */
92
+ export function hashCode(str) {
93
+ let hash = 0;
94
+ for (let i = 0; i < str.length; i++) {
95
+ const char = str.charCodeAt(i);
96
+ hash = (hash << 5) - hash + char;
97
+ hash = hash & hash;
98
+ }
99
+ return Math.abs(hash).toString(36);
100
+ }
101
+
102
+ /**
103
+ * Get canvas fingerprint for device identification
104
+ * @returns {string} Canvas fingerprint or "no-canvas" if unavailable
105
+ */
106
+ export function getCanvasFingerprint() {
107
+ try {
108
+ const canvas = document.createElement("canvas");
109
+ const ctx = canvas.getContext("2d");
110
+ ctx.textBaseline = "top";
111
+ ctx.font = "14px Arial";
112
+ ctx.fillText("LicenseSeat SDK", 2, 2);
113
+ return canvas.toDataURL().slice(-50);
114
+ } catch (e) {
115
+ return "no-canvas";
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Generate a unique device identifier based on browser characteristics
121
+ * @returns {string} Unique device identifier
122
+ */
123
+ export function generateDeviceId() {
124
+ const nav = window.navigator;
125
+ const screen = window.screen;
126
+ const data = [
127
+ nav.userAgent,
128
+ nav.language,
129
+ screen.colorDepth,
130
+ screen.width + "x" + screen.height,
131
+ new Date().getTimezoneOffset(),
132
+ nav.hardwareConcurrency,
133
+ getCanvasFingerprint(),
134
+ ].join("|");
135
+
136
+ return `web-${hashCode(data)}-${Date.now().toString(36)}`;
137
+ }
138
+
139
+ /**
140
+ * Sleep for a specified duration
141
+ * @param {number} ms - Duration in milliseconds
142
+ * @returns {Promise<void>} Resolves after the specified duration
143
+ */
144
+ export function sleep(ms) {
145
+ return new Promise((resolve) => setTimeout(resolve, ms));
146
+ }
147
+
148
+ /**
149
+ * Get CSRF token from meta tag (for browser form submissions)
150
+ * @returns {string} CSRF token or empty string if not found
151
+ */
152
+ export function getCsrfToken() {
153
+ /** @type {HTMLMetaElement|null} */
154
+ const token = document.querySelector('meta[name="csrf-token"]');
155
+ return token ? token.content : "";
156
+ }