@carchalabs/seal-electron 0.6.1 → 0.7.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/README.md +1 -1
- package/dist/index.d.ts +27 -6
- package/dist/index.js +157 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @carchalabs/seal-electron
|
|
2
2
|
|
|
3
|
-
The [Carcha Seal](https://seal.carchalabs.com)
|
|
3
|
+
The [Carcha Seal](https://seal.carchalabs.com) SDK. It runs in the Electron main process or in plain Node. It activates a license key on this device, saves the signed lease, and verifies it offline with your public keys.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
const { Seal } = require('@carchalabs/seal-electron');
|
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export declare class Seal {
|
|
|
80
80
|
/** The native lease verifier, when it trusts every key this app was given (undefined = not looked up yet). */
|
|
81
81
|
private nativeLease?;
|
|
82
82
|
private deviceKeyCache?;
|
|
83
|
+
/** The end of the operations waiting to run; see serial(). */
|
|
84
|
+
private queue;
|
|
83
85
|
constructor(opts: SealOptions);
|
|
84
86
|
private get fetch();
|
|
85
87
|
private nowSec;
|
|
@@ -89,18 +91,31 @@ export declare class Seal {
|
|
|
89
91
|
private hardware;
|
|
90
92
|
/** The saved record; null when there is none, or it cannot be read or is not a record (as no license). */
|
|
91
93
|
private load;
|
|
92
|
-
/** Replaces license.dat whole: written to
|
|
93
|
-
* leaves the previous record. */
|
|
94
|
+
/** Replaces license.dat whole: written to a temporary file of this save, then renamed over it, so a crash or full disk
|
|
95
|
+
* mid-write leaves the previous record. */
|
|
94
96
|
private save;
|
|
95
97
|
private clear;
|
|
98
|
+
/**
|
|
99
|
+
* Runs the operations that change license.dat one at a time, in the order they were called, as the native SDKs' lock
|
|
100
|
+
* does. Node.js calls writing one file from calls that overlap unsafe, and the answer to an earlier call (a slow
|
|
101
|
+
* check-in for the old key) must not replace what a later one saved (an activation with a new key). Reads
|
|
102
|
+
* (decrypt, requireEncrypted) do not wait: the file is replaced whole, so a read sees one record or the other.
|
|
103
|
+
*/
|
|
104
|
+
private serial;
|
|
96
105
|
private post;
|
|
97
106
|
private emptyState;
|
|
98
107
|
/**
|
|
99
108
|
* The device's public key, sent with activation and check-ins so the server wraps feature keys for
|
|
100
|
-
* this machine only.
|
|
101
|
-
*
|
|
109
|
+
* this machine only. Sent when the native module can also decrypt what is wrapped for it, with the
|
|
110
|
+
* matching secret it derives itself: through the compiled-in keys when they verify for this app, or,
|
|
111
|
+
* with seal-hwid 0.5+, through this app's own keys (every hosted vendor, a self-hosted server). Not
|
|
112
|
+
* with hardware turned off. Never throws.
|
|
102
113
|
*/
|
|
103
114
|
private deviceKey;
|
|
115
|
+
/** The native module's decryption with this app's own keys (seal-hwid 0.5+), or null: older module, none, or hardware off. */
|
|
116
|
+
private deviceDecrypter;
|
|
117
|
+
/** This app's public keys as SPKI PEM, for the native module. */
|
|
118
|
+
private keyPems;
|
|
104
119
|
/**
|
|
105
120
|
* The native verifier, used only when its compiled-in keys include every key this app was given:
|
|
106
121
|
* an app that trusts other keys (a self-hosted server) is checked by the JavaScript verifier alone.
|
|
@@ -115,15 +130,18 @@ export declare class Seal {
|
|
|
115
130
|
private stateFromLease;
|
|
116
131
|
/** machineName: the name to show the vendor for this device. Nothing is sent when the app gives none (host names often hold a person's name). */
|
|
117
132
|
activate(licenseKey: string, machineName?: string): Promise<LicenseState>;
|
|
133
|
+
private activateNow;
|
|
118
134
|
check(): Promise<LicenseState>;
|
|
135
|
+
private checkNow;
|
|
119
136
|
/** The state from the saved record when the server gave no usable answer; a usable lease moves lastSeen forward. */
|
|
120
137
|
private offlineCheck;
|
|
121
138
|
/**
|
|
122
139
|
* Decrypts data you encrypted for a feature (encryptForFeature in @carchalabs/seal-core, with the
|
|
123
140
|
* feature's content key from the console). Works offline: it uses the saved lease, which must be valid
|
|
124
141
|
* (or in its offline grace period), grant the feature and carry its key, and the saved license key.
|
|
125
|
-
*
|
|
126
|
-
*
|
|
142
|
+
* When the lease is wrapped for this machine's device key, the native module does the work (with its
|
|
143
|
+
* compiled-in keys or, seal-hwid 0.5+, this app's keys), so the device secret and the content key never
|
|
144
|
+
* reach JavaScript; a lease wrapped for the license key alone is opened in JavaScript. Throws a LicenseError:
|
|
127
145
|
* `unlicensed`, a lease failure reason, `clock_suspect`, `feature_not_licensed` or `decrypt_failed`.
|
|
128
146
|
*/
|
|
129
147
|
decrypt(feature: string, data: Uint8Array): Promise<Buffer>;
|
|
@@ -158,13 +176,16 @@ export declare class Seal {
|
|
|
158
176
|
* LicenseError: `invalid_file`, `wrong_product`, or a lease failure reason such as `wrong_device`.
|
|
159
177
|
*/
|
|
160
178
|
activateOffline(licenseFile: string): Promise<LicenseState>;
|
|
179
|
+
private activateOfflineNow;
|
|
161
180
|
/**
|
|
162
181
|
* Gives an offline machine's seat back: removes the license from this device and returns the
|
|
163
182
|
* deactivation file (JSON text) for the vendor, who frees the seat by uploading it in the console.
|
|
164
183
|
* Throws `unlicensed` when there is no license to remove.
|
|
165
184
|
*/
|
|
166
185
|
deactivateOffline(): Promise<string>;
|
|
186
|
+
private deactivateOfflineNow;
|
|
167
187
|
deactivate(): Promise<void>;
|
|
188
|
+
private deactivateNow;
|
|
168
189
|
static hasFeature(state: LicenseState, feature: string): boolean;
|
|
169
190
|
}
|
|
170
191
|
/** The SDK class under its earlier name (LicenseKit). Same class; use Seal in new code. */
|
package/dist/index.js
CHANGED
|
@@ -35,11 +35,44 @@ function loadHwid() {
|
|
|
35
35
|
}
|
|
36
36
|
return hwidModule;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The largest answer read, as in the native SDKs: a lease and its notices are a few kilobytes. A longer one is read as no
|
|
40
|
+
* answer, so a broken or hostile server cannot make the app hold and parse hundreds of megabytes.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_ANSWER_BYTES = 1 << 20;
|
|
43
|
+
/** The answer's body as text; null when it is longer than MAX_ANSWER_BYTES or could not be read to its end. */
|
|
44
|
+
async function readLimited(res) {
|
|
45
|
+
const declared = Number(res.headers.get('content-length') ?? NaN);
|
|
46
|
+
if (declared > MAX_ANSWER_BYTES || !res.body) {
|
|
47
|
+
await res.body?.cancel().catch(() => { });
|
|
48
|
+
return res.body ? null : '';
|
|
49
|
+
}
|
|
50
|
+
const reader = res.body.getReader();
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let length = 0;
|
|
53
|
+
try {
|
|
54
|
+
for (;;) {
|
|
55
|
+
const { done, value } = await reader.read();
|
|
56
|
+
if (done)
|
|
57
|
+
break;
|
|
58
|
+
length += value.byteLength;
|
|
59
|
+
if (length > MAX_ANSWER_BYTES) {
|
|
60
|
+
await reader.cancel().catch(() => { });
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
chunks.push(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// UTF-8 as response.json() decodes it (a byte order mark dropped).
|
|
70
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
71
|
+
}
|
|
72
|
+
/** Numbers this process's temporary files (see Seal.save). */
|
|
73
|
+
let tempCounter = 0;
|
|
38
74
|
/** The server accepts at most this many components. */
|
|
39
75
|
const MAX_COMPONENTS = 32;
|
|
40
|
-
/** The license itself is gone: forget the key too. The other refusals can be undone on the server (resume, renew,
|
|
41
|
-
* re-activate), so the key is kept and the next successful online check restores the license on its own. */
|
|
42
|
-
const FORGET_KEY = new Set(['invalid_key', 'license_revoked']);
|
|
43
76
|
const CLOCK_TOLERANCE_SECONDS = 24 * 3600;
|
|
44
77
|
/**
|
|
45
78
|
* A check-in answered 2xx whose lease fails for one of these reasons did not come from this app's license server for
|
|
@@ -128,6 +161,8 @@ class Seal {
|
|
|
128
161
|
/** The native lease verifier, when it trusts every key this app was given (undefined = not looked up yet). */
|
|
129
162
|
nativeLease;
|
|
130
163
|
deviceKeyCache;
|
|
164
|
+
/** The end of the operations waiting to run; see serial(). */
|
|
165
|
+
queue = Promise.resolve();
|
|
131
166
|
constructor(opts) {
|
|
132
167
|
this.opts = opts;
|
|
133
168
|
checkServerUrl(opts.serverUrl);
|
|
@@ -191,24 +226,53 @@ class Seal {
|
|
|
191
226
|
return null;
|
|
192
227
|
}
|
|
193
228
|
}
|
|
194
|
-
/** Replaces license.dat whole: written to
|
|
195
|
-
* leaves the previous record. */
|
|
229
|
+
/** Replaces license.dat whole: written to a temporary file of this save, then renamed over it, so a crash or full disk
|
|
230
|
+
* mid-write leaves the previous record. */
|
|
196
231
|
async save(stored) {
|
|
197
232
|
await (0, promises_1.mkdir)(this.opts.storageDir, { recursive: true });
|
|
198
233
|
const text = JSON.stringify(stored);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
234
|
+
const data = this.opts.secureStore ? await this.opts.secureStore.encrypt(text) : text;
|
|
235
|
+
// license.dat.<pid>.<n>.tmp, created only if it does not exist (flag wx; SEI CERT FIO21-C: a unique name, opened
|
|
236
|
+
// exclusively, owner-only), so two app instances saving at the same time each write and rename their own file.
|
|
237
|
+
// A name left by a process that crashed is skipped.
|
|
238
|
+
for (let attempt = 0;; attempt++) {
|
|
239
|
+
const temp = `${this.file}.${process.pid}.${tempCounter++}.tmp`;
|
|
240
|
+
try {
|
|
241
|
+
await (0, promises_1.writeFile)(temp, data, { mode: 0o600, flush: true, flag: 'wx' });
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
if (e.code === 'EEXIST') {
|
|
245
|
+
if (attempt < 100)
|
|
246
|
+
continue; // another file's name: not ours to remove
|
|
247
|
+
throw e;
|
|
248
|
+
}
|
|
249
|
+
await (0, promises_1.rm)(temp, { force: true }).catch(() => { }); // ours, cut off mid-write
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
await (0, promises_1.rename)(temp, this.file);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
await (0, promises_1.rm)(temp, { force: true }).catch(() => { });
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
207
260
|
}
|
|
208
261
|
}
|
|
209
262
|
async clear() {
|
|
210
263
|
await (0, promises_1.rm)(this.file, { force: true });
|
|
211
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Runs the operations that change license.dat one at a time, in the order they were called, as the native SDKs' lock
|
|
267
|
+
* does. Node.js calls writing one file from calls that overlap unsafe, and the answer to an earlier call (a slow
|
|
268
|
+
* check-in for the old key) must not replace what a later one saved (an activation with a new key). Reads
|
|
269
|
+
* (decrypt, requireEncrypted) do not wait: the file is replaced whole, so a read sees one record or the other.
|
|
270
|
+
*/
|
|
271
|
+
serial(operation) {
|
|
272
|
+
const run = this.queue.then(operation);
|
|
273
|
+
this.queue = run.catch(() => undefined);
|
|
274
|
+
return run;
|
|
275
|
+
}
|
|
212
276
|
async post(path, body) {
|
|
213
277
|
let res;
|
|
214
278
|
try {
|
|
@@ -225,7 +289,19 @@ class Seal {
|
|
|
225
289
|
catch {
|
|
226
290
|
throw new LicenseError('network', 'Could not reach the license server.');
|
|
227
291
|
}
|
|
228
|
-
const
|
|
292
|
+
const text = await readLimited(res);
|
|
293
|
+
// Longer than the limit, or cut off (the timeout, a broken connection): no answer, as in the native SDKs.
|
|
294
|
+
if (text === null)
|
|
295
|
+
throw new LicenseError('network', 'Could not reach the license server.');
|
|
296
|
+
let json;
|
|
297
|
+
try {
|
|
298
|
+
json = JSON.parse(text);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
json = {};
|
|
302
|
+
}
|
|
303
|
+
if (typeof json !== 'object' || json === null)
|
|
304
|
+
json = {};
|
|
229
305
|
if (!res.ok)
|
|
230
306
|
throw new LicenseError(json.error ?? 'server_error', json.message ?? `License server error (${res.status})`, res.status);
|
|
231
307
|
return json;
|
|
@@ -235,17 +311,20 @@ class Seal {
|
|
|
235
311
|
}
|
|
236
312
|
/**
|
|
237
313
|
* The device's public key, sent with activation and check-ins so the server wraps feature keys for
|
|
238
|
-
* this machine only.
|
|
239
|
-
*
|
|
314
|
+
* this machine only. Sent when the native module can also decrypt what is wrapped for it, with the
|
|
315
|
+
* matching secret it derives itself: through the compiled-in keys when they verify for this app, or,
|
|
316
|
+
* with seal-hwid 0.5+, through this app's own keys (every hosted vendor, a self-hosted server). Not
|
|
317
|
+
* with hardware turned off. Never throws.
|
|
240
318
|
*/
|
|
241
319
|
deviceKey() {
|
|
242
320
|
if (this.deviceKeyCache === undefined) {
|
|
243
321
|
let key = null;
|
|
244
322
|
try {
|
|
323
|
+
const hwid = this.opts.hardware === false ? null : loadHwid();
|
|
245
324
|
const native = this.opts.hardware === false ? null : this.native();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
325
|
+
const decrypts = (native && typeof native.decryptFeature === 'function') || this.deviceDecrypter() !== null;
|
|
326
|
+
if (hwid && typeof hwid.devicePublicKey === 'function' && decrypts)
|
|
327
|
+
key = hwid.devicePublicKey(`licensekit:${this.opts.product}`);
|
|
249
328
|
}
|
|
250
329
|
catch {
|
|
251
330
|
key = null;
|
|
@@ -254,6 +333,17 @@ class Seal {
|
|
|
254
333
|
}
|
|
255
334
|
return this.deviceKeyCache ? { devicePublicKey: this.deviceKeyCache } : {};
|
|
256
335
|
}
|
|
336
|
+
/** The native module's decryption with this app's own keys (seal-hwid 0.5+), or null: older module, none, or hardware off. */
|
|
337
|
+
deviceDecrypter() {
|
|
338
|
+
if (this.opts.hardware === false)
|
|
339
|
+
return null;
|
|
340
|
+
const hwid = loadHwid();
|
|
341
|
+
return hwid && typeof hwid.decryptFeatureWithKeys === 'function' ? hwid : null;
|
|
342
|
+
}
|
|
343
|
+
/** This app's public keys as SPKI PEM, for the native module. */
|
|
344
|
+
keyPems() {
|
|
345
|
+
return [...this.keys.values()].map((key) => key.export({ type: 'spki', format: 'pem' }).toString());
|
|
346
|
+
}
|
|
257
347
|
/**
|
|
258
348
|
* The native verifier, used only when its compiled-in keys include every key this app was given:
|
|
259
349
|
* an app that trusts other keys (a self-hosted server) is checked by the JavaScript verifier alone.
|
|
@@ -334,7 +424,10 @@ class Seal {
|
|
|
334
424
|
return this.emptyState('invalid', offline, { reason: strict.reason, message: REASON_TEXT[strict.reason], keyHint });
|
|
335
425
|
}
|
|
336
426
|
/** machineName: the name to show the vendor for this device. Nothing is sent when the app gives none (host names often hold a person's name). */
|
|
337
|
-
|
|
427
|
+
activate(licenseKey, machineName) {
|
|
428
|
+
return this.serial(() => this.activateNow(licenseKey, machineName));
|
|
429
|
+
}
|
|
430
|
+
async activateNow(licenseKey, machineName) {
|
|
338
431
|
const fingerprint = await this.fingerprint();
|
|
339
432
|
const key = licenseKey.trim().toUpperCase();
|
|
340
433
|
const response = await this.post('/v1/activate', {
|
|
@@ -355,7 +448,10 @@ class Seal {
|
|
|
355
448
|
await this.save(stored);
|
|
356
449
|
return state;
|
|
357
450
|
}
|
|
358
|
-
|
|
451
|
+
check() {
|
|
452
|
+
return this.serial(() => this.checkNow());
|
|
453
|
+
}
|
|
454
|
+
async checkNow() {
|
|
359
455
|
try {
|
|
360
456
|
await this.fingerprint();
|
|
361
457
|
}
|
|
@@ -390,11 +486,11 @@ class Seal {
|
|
|
390
486
|
}
|
|
391
487
|
catch (e) {
|
|
392
488
|
if (e instanceof LicenseError && DEFINITIVE.has(e.code)) {
|
|
393
|
-
// Persist the refusal: drop the signed lease so going offline cannot bring the license back.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
489
|
+
// Persist the refusal: drop the signed lease so going offline cannot bring the license back. The key is kept, also
|
|
490
|
+
// for invalid_key and license_revoked: a refusal is not signed, so anything that answers on the connection (a
|
|
491
|
+
// TLS-inspecting proxy) could send one, and the next check-in the server answers with a lease restores the
|
|
492
|
+
// license. A refusal that is real stays one: the server refuses the kept key again.
|
|
493
|
+
await this.save({ licenseKey: stored.licenseKey, lastSeen: stored.lastSeen, refused: { code: e.code, message: e.message, at: this.nowSec() } });
|
|
398
494
|
const keyHint = `${stored.licenseKey.slice(0, 7)}…${stored.licenseKey.slice(-4)}`;
|
|
399
495
|
return this.emptyState('invalid', false, { reason: e.code, message: e.message, keyHint });
|
|
400
496
|
}
|
|
@@ -428,8 +524,9 @@ class Seal {
|
|
|
428
524
|
* Decrypts data you encrypted for a feature (encryptForFeature in @carchalabs/seal-core, with the
|
|
429
525
|
* feature's content key from the console). Works offline: it uses the saved lease, which must be valid
|
|
430
526
|
* (or in its offline grace period), grant the feature and carry its key, and the saved license key.
|
|
431
|
-
*
|
|
432
|
-
*
|
|
527
|
+
* When the lease is wrapped for this machine's device key, the native module does the work (with its
|
|
528
|
+
* compiled-in keys or, seal-hwid 0.5+, this app's keys), so the device secret and the content key never
|
|
529
|
+
* reach JavaScript; a lease wrapped for the license key alone is opened in JavaScript. Throws a LicenseError:
|
|
433
530
|
* `unlicensed`, a lease failure reason, `clock_suspect`, `feature_not_licensed` or `decrypt_failed`.
|
|
434
531
|
*/
|
|
435
532
|
async decrypt(feature, data) {
|
|
@@ -499,10 +596,27 @@ class Seal {
|
|
|
499
596
|
const graceSeconds = state.status === 'grace' ? (claims?.grc ?? (this.opts.graceDays ?? DEFAULT_GRACE_DAYS) * 86400) : 0;
|
|
500
597
|
const check = { product: this.opts.product, fingerprintHash, now: this.nowSec(), ...(graceSeconds ? { graceSeconds } : {}) };
|
|
501
598
|
const native = this.native();
|
|
599
|
+
// A lease wrapped for this machine's device key (epk) opens only in the native module, which holds the device
|
|
600
|
+
// secret: through the compiled-in keys when they verify for this app, else through this app's keys (seal-hwid 0.5+).
|
|
601
|
+
const deviceBound = claims?.epk !== undefined;
|
|
602
|
+
const withKeys = deviceBound && !(native && typeof native.decryptFeature === 'function') ? this.deviceDecrypter() : null;
|
|
603
|
+
const pems = withKeys ? this.keyPems() : [];
|
|
502
604
|
return (data) => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
605
|
+
let result;
|
|
606
|
+
if (native && typeof native.decryptFeature === 'function') {
|
|
607
|
+
result = native.decryptFeature(leased.token, check, leased.licenseKey, feature, Buffer.from(data));
|
|
608
|
+
}
|
|
609
|
+
else if (withKeys) {
|
|
610
|
+
try {
|
|
611
|
+
result = withKeys.decryptFeatureWithKeys(leased.token, check, pems, leased.licenseKey, feature, Buffer.from(data));
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
result = { ok: false, reason: 'decrypt_failed' };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
result = (0, seal_core_1.decryptFeature)(leased.token, { keys: this.keys, ...check }, leased.licenseKey, feature, Buffer.from(data));
|
|
619
|
+
}
|
|
506
620
|
if (result.ok && result.data)
|
|
507
621
|
return Buffer.from(result.data);
|
|
508
622
|
if (result.reason === 'not_licensed')
|
|
@@ -539,7 +653,10 @@ class Seal {
|
|
|
539
653
|
* check() works without the server until the lease's renewal date plus the offline grace. Throws a
|
|
540
654
|
* LicenseError: `invalid_file`, `wrong_product`, or a lease failure reason such as `wrong_device`.
|
|
541
655
|
*/
|
|
542
|
-
|
|
656
|
+
activateOffline(licenseFile) {
|
|
657
|
+
return this.serial(() => this.activateOfflineNow(licenseFile));
|
|
658
|
+
}
|
|
659
|
+
async activateOfflineNow(licenseFile) {
|
|
543
660
|
await this.fingerprint();
|
|
544
661
|
let file;
|
|
545
662
|
try {
|
|
@@ -566,14 +683,20 @@ class Seal {
|
|
|
566
683
|
* deactivation file (JSON text) for the vendor, who frees the seat by uploading it in the console.
|
|
567
684
|
* Throws `unlicensed` when there is no license to remove.
|
|
568
685
|
*/
|
|
569
|
-
|
|
686
|
+
deactivateOffline() {
|
|
687
|
+
return this.serial(() => this.deactivateOfflineNow());
|
|
688
|
+
}
|
|
689
|
+
async deactivateOfflineNow() {
|
|
570
690
|
const stored = await this.load();
|
|
571
691
|
if (!stored?.token)
|
|
572
692
|
throw new LicenseError('unlicensed', 'This device has no license.');
|
|
573
693
|
await this.clear();
|
|
574
694
|
return JSON.stringify({ format: OFFLINE_DEACTIVATION_FORMAT, version: 1, product: this.opts.product, token: stored.token }, null, 2);
|
|
575
695
|
}
|
|
576
|
-
|
|
696
|
+
deactivate() {
|
|
697
|
+
return this.serial(() => this.deactivateNow());
|
|
698
|
+
}
|
|
699
|
+
async deactivateNow() {
|
|
577
700
|
const stored = await this.load();
|
|
578
701
|
if (!stored)
|
|
579
702
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carchalabs/seal-electron",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Carcha Seal (formerly LicenseKit) SDK for the Electron main process and Node: activation, offline verification, grace period",
|
|
5
5
|
"keywords": ["electron", "licensing", "license-key", "software-licensing", "offline-licensing", "node-locked", "ed25519", "jws", "carcha-seal"],
|
|
6
6
|
"homepage": "https://seal.carchalabs.com/docs/",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@carchalabs/seal-core": "0.5.1"
|
|
19
19
|
},
|
|
20
20
|
"optionalDependencies": {
|
|
21
|
-
"@carchalabs/seal-hwid": "0.
|
|
21
|
+
"@carchalabs/seal-hwid": "0.5.0"
|
|
22
22
|
},
|
|
23
23
|
"engines": { "node": ">=22.13" },
|
|
24
24
|
"publishConfig": { "access": "public" },
|