@licenseseat/js 0.1.0 → 0.2.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/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,148 @@
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://licenseseat.com/api"] - 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
+ * Note: API returns only key, expires_at, and metadata. Name/description are not provided.
64
+ * @typedef {Object} Entitlement
65
+ * @property {string} key - Unique entitlement key
66
+ * @property {string|null} expires_at - ISO8601 expiration timestamp
67
+ * @property {Object|null} metadata - Additional metadata
68
+ */
69
+
70
+ /**
71
+ * Validation result from API or offline verification
72
+ * @typedef {Object} ValidationResult
73
+ * @property {boolean} valid - Whether the license is valid
74
+ * @property {boolean} [offline] - Whether this was an offline validation
75
+ * @property {string} [reason] - Reason for invalid status (online)
76
+ * @property {string} [reason_code] - Machine-readable reason code (offline)
77
+ * @property {Entitlement[]} [active_entitlements] - List of active entitlements
78
+ * @property {boolean} [optimistic] - Whether this is an optimistic validation (pending server confirmation)
79
+ */
80
+
81
+ /**
82
+ * Entitlement check result
83
+ * @typedef {Object} EntitlementCheckResult
84
+ * @property {boolean} active - Whether the entitlement is active
85
+ * @property {string} [reason] - Reason if not active ("no_license" | "not_found" | "expired")
86
+ * @property {string} [expires_at] - ISO8601 expiration timestamp if expired
87
+ * @property {Entitlement} [entitlement] - Full entitlement object if active
88
+ */
89
+
90
+ /**
91
+ * License status object
92
+ * @typedef {Object} LicenseStatus
93
+ * @property {string} status - Status string ("inactive" | "pending" | "invalid" | "offline-invalid" | "offline-valid" | "active")
94
+ * @property {string} [message] - Human-readable status message
95
+ * @property {string} [license] - License key (if active)
96
+ * @property {string} [device] - Device identifier (if active)
97
+ * @property {string} [activated_at] - ISO8601 activation timestamp
98
+ * @property {string} [last_validated] - ISO8601 last validation timestamp
99
+ * @property {Entitlement[]} [entitlements] - List of active entitlements
100
+ */
101
+
102
+ /**
103
+ * Offline license payload
104
+ * @typedef {Object} OfflineLicensePayload
105
+ * @property {number} [v] - Payload version (currently 1)
106
+ * @property {string} [lic_k] - License key
107
+ * @property {string} [prod_s] - Product slug
108
+ * @property {string} [plan_k] - License plan key
109
+ * @property {string|null} [exp_at] - ISO8601 expiration timestamp (null for perpetual)
110
+ * @property {number|null} [sl] - Seat limit (null for unlimited)
111
+ * @property {string} [kid] - Key ID for public key lookup
112
+ * @property {Array<{key: string, expires_at: string|null, metadata: Object|null}>} [active_ents] - Active entitlements
113
+ * @property {Array<{key: string, expires_at: string|null, metadata: Object|null}>} [active_entitlements] - Active entitlements (alternative key)
114
+ * @property {Object} [metadata] - Additional metadata
115
+ */
116
+
117
+ /**
118
+ * Signed offline license data
119
+ * @typedef {Object} SignedOfflineLicense
120
+ * @property {OfflineLicensePayload} payload - The license payload
121
+ * @property {string} signature_b64u - Base64URL-encoded Ed25519 signature
122
+ * @property {string} [kid] - Key ID for public key lookup
123
+ */
124
+
125
+ /**
126
+ * Event callback function
127
+ * @callback EventCallback
128
+ * @param {*} data - Event data
129
+ * @returns {void}
130
+ */
131
+
132
+ /**
133
+ * Event unsubscribe function
134
+ * @callback EventUnsubscribe
135
+ * @returns {void}
136
+ */
137
+
138
+ /**
139
+ * API Error data
140
+ * @typedef {Object} APIErrorData
141
+ * @property {string} [error] - Error message
142
+ * @property {string} [reason_code] - Machine-readable reason code (e.g., "license_not_found", "expired", "revoked")
143
+ * @property {string} [code] - Legacy error code (deprecated, use reason_code)
144
+ * @property {Object} [details] - Additional error details
145
+ */
146
+
147
+ // Export empty object to make this a module
148
+ export {};
package/src/utils.js ADDED
@@ -0,0 +1,154 @@
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
+ expires_at: e.expires_at ?? null,
19
+ metadata: e.metadata ?? null,
20
+ }));
21
+ }
22
+
23
+ /**
24
+ * Constant-time string comparison to mitigate timing attacks
25
+ * @param {string} [a=""] - First string
26
+ * @param {string} [b=""] - Second string
27
+ * @returns {boolean} True if strings are equal
28
+ */
29
+ export function constantTimeEqual(a = "", b = "") {
30
+ if (a.length !== b.length) return false;
31
+ let res = 0;
32
+ for (let i = 0; i < a.length; i++) {
33
+ res |= a.charCodeAt(i) ^ b.charCodeAt(i);
34
+ }
35
+ return res === 0;
36
+ }
37
+
38
+ /**
39
+ * Generate a canonical JSON string from an object (keys sorted)
40
+ * This is crucial for consistent signature verification.
41
+ * @param {Object} obj - The object to stringify
42
+ * @returns {string} Canonical JSON string
43
+ */
44
+ export function canonicalJsonStringify(obj) {
45
+ // canonical-json exports the stringify function directly as default
46
+ /** @type {Function|undefined} */
47
+ const stringify = typeof CJSON === "function" ? CJSON : (CJSON && typeof CJSON === "object" ? /** @type {any} */ (CJSON).stringify : undefined);
48
+ if (!stringify || typeof stringify !== "function") {
49
+ console.warn(
50
+ "[LicenseSeat SDK] canonical-json library not loaded correctly. Falling back to basic JSON.stringify. Signature verification might be unreliable if server uses different canonicalization."
51
+ );
52
+ try {
53
+ const sortedObj = {};
54
+ Object.keys(obj)
55
+ .sort()
56
+ .forEach((key) => {
57
+ sortedObj[key] = obj[key];
58
+ });
59
+ return JSON.stringify(sortedObj);
60
+ } catch (e) {
61
+ return JSON.stringify(obj);
62
+ }
63
+ }
64
+ return stringify(obj);
65
+ }
66
+
67
+ /**
68
+ * Decode a Base64URL string to a Uint8Array
69
+ * @param {string} base64UrlString - The Base64URL encoded string
70
+ * @returns {Uint8Array} Decoded bytes
71
+ */
72
+ export function base64UrlDecode(base64UrlString) {
73
+ let base64 = base64UrlString.replace(/-/g, "+").replace(/_/g, "/");
74
+ while (base64.length % 4) {
75
+ base64 += "=";
76
+ }
77
+ const raw = window.atob(base64);
78
+ const outputArray = new Uint8Array(raw.length);
79
+ for (let i = 0; i < raw.length; ++i) {
80
+ outputArray[i] = raw.charCodeAt(i);
81
+ }
82
+ return outputArray;
83
+ }
84
+
85
+ /**
86
+ * Simple hash function for generating device fingerprints
87
+ * @param {string} str - String to hash
88
+ * @returns {string} Base36 encoded hash
89
+ */
90
+ export function hashCode(str) {
91
+ let hash = 0;
92
+ for (let i = 0; i < str.length; i++) {
93
+ const char = str.charCodeAt(i);
94
+ hash = (hash << 5) - hash + char;
95
+ hash = hash & hash;
96
+ }
97
+ return Math.abs(hash).toString(36);
98
+ }
99
+
100
+ /**
101
+ * Get canvas fingerprint for device identification
102
+ * @returns {string} Canvas fingerprint or "no-canvas" if unavailable
103
+ */
104
+ export function getCanvasFingerprint() {
105
+ try {
106
+ const canvas = document.createElement("canvas");
107
+ const ctx = canvas.getContext("2d");
108
+ ctx.textBaseline = "top";
109
+ ctx.font = "14px Arial";
110
+ ctx.fillText("LicenseSeat SDK", 2, 2);
111
+ return canvas.toDataURL().slice(-50);
112
+ } catch (e) {
113
+ return "no-canvas";
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Generate a unique device identifier based on browser characteristics
119
+ * @returns {string} Unique device identifier
120
+ */
121
+ export function generateDeviceId() {
122
+ const nav = window.navigator;
123
+ const screen = window.screen;
124
+ const data = [
125
+ nav.userAgent,
126
+ nav.language,
127
+ screen.colorDepth,
128
+ screen.width + "x" + screen.height,
129
+ new Date().getTimezoneOffset(),
130
+ nav.hardwareConcurrency,
131
+ getCanvasFingerprint(),
132
+ ].join("|");
133
+
134
+ return `web-${hashCode(data)}-${Date.now().toString(36)}`;
135
+ }
136
+
137
+ /**
138
+ * Sleep for a specified duration
139
+ * @param {number} ms - Duration in milliseconds
140
+ * @returns {Promise<void>} Resolves after the specified duration
141
+ */
142
+ export function sleep(ms) {
143
+ return new Promise((resolve) => setTimeout(resolve, ms));
144
+ }
145
+
146
+ /**
147
+ * Get CSRF token from meta tag (for browser form submissions)
148
+ * @returns {string} CSRF token or empty string if not found
149
+ */
150
+ export function getCsrfToken() {
151
+ /** @type {HTMLMetaElement|null} */
152
+ const token = document.querySelector('meta[name="csrf-token"]');
153
+ return token ? token.content : "";
154
+ }