@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/README.md +684 -83
- package/dist/index.js +775 -1526
- package/dist/types/LicenseSeat.d.ts +309 -0
- package/dist/types/LicenseSeat.d.ts.map +1 -0
- package/dist/types/cache.d.ts +93 -0
- package/dist/types/cache.d.ts.map +1 -0
- package/dist/types/errors.d.ts +58 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/types.d.ts +342 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils.d.ts +54 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +44 -8
- package/src/LicenseSeat.js +1247 -0
- package/src/cache.js +189 -0
- package/src/errors.js +77 -0
- package/src/index.js +62 -0
- package/src/types.js +148 -0
- package/src/utils.js +154 -0
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
|
+
}
|