@carchalabs/licensekit-electron 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bugra Omer Aydemir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # @carchalabs/licensekit-electron
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The [LicenseKit](https://licensekit.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
+
5
+ ```js
6
+ const { LicenseKit } = require('@carchalabs/licensekit-electron');
7
+
8
+ const kit = new LicenseKit({
9
+ serverUrl: 'https://licenses.example.com',
10
+ product: 'example-studio',
11
+ publicKey: [currentKeyPem, nextKeyPem],
12
+ storageDir: app.getPath('userData'),
13
+ secureStore: {
14
+ encrypt: (s) => safeStorage.encryptString(s),
15
+ decrypt: (b) => safeStorage.decryptString(b),
16
+ },
17
+ });
18
+
19
+ const state = await kit.check();
20
+ if (state.status === 'unlicensed') await kit.activate(userEnteredKey);
21
+ if (LicenseKit.hasFeature(state, 'export-pdf')) exportPdf();
22
+ ```
23
+
24
+ `check()` returns one of `valid`, `grace`, `invalid`, `unlicensed` or `clock_suspect`, with `offline` and `storageProtected` flags. `activate(key, machineName)` sends the device name you pass; the default is the computer's host name. `deactivate()` releases the seat.
25
+
26
+ The SDK sends the license key, a per-product SHA-256 hash of the operating system's machine ID, the device name, the platform and your app version. The raw machine ID does not leave the computer.
27
+
28
+ Full reference: [licensekit.carchalabs.com/docs](https://licensekit.carchalabs.com/docs/#sdk). Requires Node 22.13 or later. MIT License.
@@ -0,0 +1,2 @@
1
+ /** Reads a stable, OS-provided machine id. Falls back to hardware traits if unavailable. */
2
+ export declare function readMachineId(): Promise<string>;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readMachineId = readMachineId;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_os_1 = require("node:os");
7
+ const node_util_1 = require("node:util");
8
+ const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
9
+ /** Reads a stable, OS-provided machine id. Falls back to hardware traits if unavailable. */
10
+ async function readMachineId() {
11
+ try {
12
+ const os = (0, node_os_1.platform)();
13
+ if (os === 'darwin') {
14
+ const { stdout } = await run('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], { timeout: 3000 });
15
+ const m = stdout.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
16
+ if (m)
17
+ return `mac:${m[1]}`;
18
+ }
19
+ else if (os === 'win32') {
20
+ const { stdout } = await run('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], {
21
+ timeout: 3000,
22
+ windowsHide: true,
23
+ });
24
+ const m = stdout.match(/MachineGuid\s+REG_SZ\s+([\w-]+)/i);
25
+ if (m)
26
+ return `win:${m[1]}`;
27
+ }
28
+ else {
29
+ for (const f of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
30
+ const id = (await (0, promises_1.readFile)(f, 'utf8').catch(() => '')).trim();
31
+ if (id)
32
+ return `linux:${id}`;
33
+ }
34
+ }
35
+ }
36
+ catch {
37
+ /* fall through */
38
+ }
39
+ return `fallback:${fallbackTraits()}`;
40
+ }
41
+ function fallbackTraits() {
42
+ const macs = Object.values((0, node_os_1.networkInterfaces)())
43
+ .flat()
44
+ .filter((i) => i && !i.internal && i.mac && i.mac !== '00:00:00:00:00:00')
45
+ .map((i) => i.mac)
46
+ .sort();
47
+ return [(0, node_os_1.hostname)(), (0, node_os_1.cpus)()[0]?.model ?? '', macs[0] ?? ''].join('|');
48
+ }
49
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":";;AAQA,sCAwBC;AAhCD,2DAA8C;AAC9C,+CAA4C;AAC5C,qCAAsE;AACtE,yCAAsC;AAEtC,MAAM,GAAG,GAAG,IAAA,qBAAS,EAAC,6BAAQ,CAAC,CAAC;AAEhC,4FAA4F;AACrF,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,IAAA,kBAAQ,GAAE,CAAC;QACtB,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;YACpB,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,wBAAwB,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACnG,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;YAC3D,IAAI,CAAC;gBAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9B,CAAC;aAAM,IAAI,EAAE,KAAK,OAAO,EAAE,CAAC;YAC1B,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,yCAAyC,EAAE,IAAI,EAAE,aAAa,CAAC,EAAE;gBAC7G,OAAO,EAAE,IAAI;gBACb,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;YAC3D,IAAI,CAAC;gBAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,0BAA0B,CAAC,EAAE,CAAC;gBAChE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAA,mBAAQ,EAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC9D,IAAI,EAAE;oBAAE,OAAO,SAAS,EAAE,EAAE,CAAC;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,kBAAkB;IACpB,CAAC;IACD,OAAO,YAAY,cAAc,EAAE,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,cAAc;IACrB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAA,2BAAiB,GAAE,CAAC;SAC5C,IAAI,EAAE;SACN,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,mBAAmB,CAAC;SACzE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAE,CAAC,GAAG,CAAC;SAClB,IAAI,EAAE,CAAC;IACV,OAAO,CAAC,IAAA,kBAAQ,GAAE,EAAE,IAAA,cAAI,GAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvE,CAAC"}
@@ -0,0 +1,60 @@
1
+ import { readMachineId } from './fingerprint';
2
+ export { readMachineId };
3
+ export type LicenseStatus = 'valid' | 'grace' | 'invalid' | 'unlicensed' | 'clock_suspect';
4
+ export interface LicenseState {
5
+ status: LicenseStatus;
6
+ offline: boolean;
7
+ storageProtected: boolean;
8
+ reason?: string;
9
+ message?: string;
10
+ customer?: string;
11
+ features: string[];
12
+ expiresAt: string | null;
13
+ maintenanceUntil?: string | null;
14
+ revalidateBy?: string;
15
+ keyHint?: string;
16
+ }
17
+ export interface SecureStore {
18
+ encrypt(plain: string): Buffer | Promise<Buffer>;
19
+ decrypt(cipher: Buffer): string | Promise<string>;
20
+ isProtected?(): boolean;
21
+ }
22
+ export interface LicenseKitOptions {
23
+ serverUrl: string;
24
+ product: string;
25
+ publicKey: string | string[];
26
+ storageDir: string;
27
+ appVersion?: string;
28
+ graceDays?: number;
29
+ secureStore?: SecureStore;
30
+ requestTimeoutMs?: number;
31
+ fetchImpl?: typeof fetch;
32
+ machineId?: () => Promise<string>;
33
+ now?: () => number;
34
+ }
35
+ export declare class LicenseError extends Error {
36
+ readonly code: string;
37
+ readonly status?: number | undefined;
38
+ constructor(code: string, message: string, status?: number | undefined);
39
+ }
40
+ export declare class LicenseKit {
41
+ private readonly opts;
42
+ private readonly file;
43
+ private readonly keys;
44
+ private fingerprintCache?;
45
+ constructor(opts: LicenseKitOptions);
46
+ private get fetch();
47
+ private nowSec;
48
+ private get storageProtected();
49
+ fingerprint(): Promise<string>;
50
+ private load;
51
+ private save;
52
+ private clear;
53
+ private post;
54
+ private emptyState;
55
+ private stateFromLease;
56
+ activate(licenseKey: string, machineName?: string): Promise<LicenseState>;
57
+ check(): Promise<LicenseState>;
58
+ deactivate(): Promise<void>;
59
+ static hasFeature(state: LicenseState, feature: string): boolean;
60
+ }
package/dist/index.js ADDED
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LicenseKit = exports.LicenseError = exports.readMachineId = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_os_1 = require("node:os");
6
+ const node_path_1 = require("node:path");
7
+ const licensekit_core_1 = require("@carchalabs/licensekit-core");
8
+ const fingerprint_1 = require("./fingerprint");
9
+ Object.defineProperty(exports, "readMachineId", { enumerable: true, get: function () { return fingerprint_1.readMachineId; } });
10
+ const DEFINITIVE = new Set(['invalid_key', 'not_activated', 'license_suspended', 'license_revoked', 'license_expired']);
11
+ const CLOCK_TOLERANCE_SECONDS = 24 * 3600;
12
+ const DEFAULT_GRACE_DAYS = 7;
13
+ const CLOCK_MESSAGE = 'The date and time on this computer look wrong. Correct them, then connect to the internet.';
14
+ class LicenseError extends Error {
15
+ code;
16
+ status;
17
+ constructor(code, message, status) {
18
+ super(message);
19
+ this.code = code;
20
+ this.status = status;
21
+ this.name = 'LicenseError';
22
+ }
23
+ }
24
+ exports.LicenseError = LicenseError;
25
+ const REASON_TEXT = {
26
+ malformed: 'The saved license is damaged. Please activate again.',
27
+ bad_signature: 'The saved license was modified. Please activate again.',
28
+ unsupported_algorithm: 'Please update the app.',
29
+ wrong_type: 'Please update the app.',
30
+ unknown_key: 'Please update the app.',
31
+ wrong_issuer: 'This license was issued by a different license server.',
32
+ not_yet_valid: CLOCK_MESSAGE,
33
+ wrong_product: 'This license is for a different product.',
34
+ wrong_device: 'This license is activated on another device.',
35
+ expired: 'Your license has expired.',
36
+ revalidation_required: 'Connect to the internet to confirm your license.',
37
+ };
38
+ const toIso = (seconds) => new Date(seconds * 1000).toISOString();
39
+ class LicenseKit {
40
+ opts;
41
+ file;
42
+ keys;
43
+ fingerprintCache;
44
+ constructor(opts) {
45
+ this.opts = opts;
46
+ this.file = (0, node_path_1.join)(opts.storageDir, 'license.dat');
47
+ this.keys = (0, licensekit_core_1.keySet)(Array.isArray(opts.publicKey) ? opts.publicKey : [opts.publicKey]);
48
+ }
49
+ get fetch() {
50
+ return this.opts.fetchImpl ?? fetch;
51
+ }
52
+ nowSec() {
53
+ return Math.floor((this.opts.now?.() ?? Date.now()) / 1000);
54
+ }
55
+ get storageProtected() {
56
+ const store = this.opts.secureStore;
57
+ return store !== undefined && (store.isProtected?.() ?? true);
58
+ }
59
+ async fingerprint() {
60
+ if (!this.fingerprintCache) {
61
+ const raw = await (this.opts.machineId ?? fingerprint_1.readMachineId)();
62
+ this.fingerprintCache = (0, licensekit_core_1.hashFingerprint)(raw, this.opts.product);
63
+ }
64
+ return this.fingerprintCache;
65
+ }
66
+ async load() {
67
+ try {
68
+ const buf = await (0, promises_1.readFile)(this.file);
69
+ const text = this.opts.secureStore ? await this.opts.secureStore.decrypt(buf) : buf.toString('utf8');
70
+ return JSON.parse(text);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ async save(stored) {
77
+ await (0, promises_1.mkdir)(this.opts.storageDir, { recursive: true });
78
+ const text = JSON.stringify(stored);
79
+ await (0, promises_1.writeFile)(this.file, this.opts.secureStore ? await this.opts.secureStore.encrypt(text) : text, { mode: 0o600 });
80
+ }
81
+ async clear() {
82
+ await (0, promises_1.rm)(this.file, { force: true });
83
+ }
84
+ async post(path, body) {
85
+ let res;
86
+ try {
87
+ res = await this.fetch(this.opts.serverUrl.replace(/\/+$/, '') + path, {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify(body),
91
+ signal: AbortSignal.timeout(this.opts.requestTimeoutMs ?? 8000),
92
+ });
93
+ }
94
+ catch {
95
+ throw new LicenseError('network', 'Could not reach the license server.');
96
+ }
97
+ const json = await res.json().catch(() => ({}));
98
+ if (!res.ok)
99
+ throw new LicenseError(json.error ?? 'server_error', json.message ?? `License server error (${res.status})`, res.status);
100
+ return json;
101
+ }
102
+ emptyState(status, offline, extra = {}) {
103
+ return { status, offline, storageProtected: this.storageProtected, features: [], expiresAt: null, ...extra };
104
+ }
105
+ stateFromLease(stored, offline) {
106
+ const fingerprintHash = this.fingerprintCache;
107
+ const now = this.nowSec();
108
+ const keyHint = `${stored.licenseKey.slice(0, 7)}…${stored.licenseKey.slice(-4)}`;
109
+ if (offline && stored.lastSeen !== undefined && now < stored.lastSeen - CLOCK_TOLERANCE_SECONDS) {
110
+ return this.emptyState('clock_suspect', offline, { reason: 'clock_rollback', message: CLOCK_MESSAGE, keyHint });
111
+ }
112
+ const verifyOptions = { keys: this.keys, product: this.opts.product, fingerprintHash, now };
113
+ const strict = (0, licensekit_core_1.verifyLease)(stored.token, verifyOptions);
114
+ const fromClaims = (claims) => ({
115
+ customer: claims.cus,
116
+ features: claims.feat,
117
+ expiresAt: claims.exp === undefined ? null : toIso(claims.exp),
118
+ maintenanceUntil: claims.mnt === undefined ? null : toIso(claims.mnt),
119
+ storageProtected: this.storageProtected,
120
+ keyHint,
121
+ });
122
+ if (strict.ok) {
123
+ return { status: 'valid', offline, ...fromClaims(strict.claims), revalidateBy: toIso(strict.claims.rev) };
124
+ }
125
+ if (strict.reason === 'not_yet_valid') {
126
+ return this.emptyState('clock_suspect', offline, { reason: 'lease_from_future', message: CLOCK_MESSAGE, keyHint });
127
+ }
128
+ if (strict.reason === 'revalidation_required' && strict.claims) {
129
+ const grace = strict.claims.grc ?? (this.opts.graceDays ?? DEFAULT_GRACE_DAYS) * 86400;
130
+ const lenient = (0, licensekit_core_1.verifyLease)(stored.token, { ...verifyOptions, graceSeconds: grace });
131
+ if (lenient.ok) {
132
+ return {
133
+ status: 'grace',
134
+ offline,
135
+ reason: 'offline_grace',
136
+ message: 'Working offline. Connect to the internet soon to keep using the app.',
137
+ ...fromClaims(lenient.claims),
138
+ revalidateBy: toIso(Math.min(lenient.claims.rev + grace, lenient.claims.exp ?? Infinity)),
139
+ };
140
+ }
141
+ }
142
+ return this.emptyState('invalid', offline, { reason: strict.reason, message: REASON_TEXT[strict.reason], keyHint });
143
+ }
144
+ async activate(licenseKey, machineName = (0, node_os_1.hostname)()) {
145
+ const fingerprint = await this.fingerprint();
146
+ const key = licenseKey.trim().toUpperCase();
147
+ const response = await this.post('/v1/activate', {
148
+ licenseKey: key,
149
+ product: this.opts.product,
150
+ fingerprint,
151
+ machineName: machineName.slice(0, 80),
152
+ platform: (0, node_os_1.platform)(),
153
+ appVersion: this.opts.appVersion,
154
+ });
155
+ const stored = { licenseKey: key, token: response.token, lastSeen: this.nowSec() };
156
+ const state = this.stateFromLease(stored, false);
157
+ if (state.status !== 'valid')
158
+ throw new LicenseError(state.status === 'clock_suspect' ? 'clock_suspect' : state.reason ?? 'invalid', state.message ?? 'Server returned an unusable license');
159
+ await this.save(stored);
160
+ return state;
161
+ }
162
+ async check() {
163
+ await this.fingerprint();
164
+ const stored = await this.load();
165
+ if (!stored)
166
+ return this.emptyState('unlicensed', false);
167
+ try {
168
+ const response = await this.post('/v1/validate', {
169
+ licenseKey: stored.licenseKey,
170
+ product: this.opts.product,
171
+ fingerprint: this.fingerprintCache,
172
+ appVersion: this.opts.appVersion,
173
+ });
174
+ const fresh = { ...stored, token: response.token, lastSeen: this.nowSec() };
175
+ await this.save(fresh);
176
+ return this.stateFromLease(fresh, false);
177
+ }
178
+ catch (e) {
179
+ if (e instanceof LicenseError && DEFINITIVE.has(e.code)) {
180
+ return this.emptyState('invalid', false, { reason: e.code, message: e.message });
181
+ }
182
+ const state = this.stateFromLease(stored, true);
183
+ if (state.status === 'valid' || state.status === 'grace') {
184
+ await this.save({ ...stored, lastSeen: Math.max(stored.lastSeen ?? 0, this.nowSec()) });
185
+ }
186
+ return state;
187
+ }
188
+ }
189
+ async deactivate() {
190
+ const stored = await this.load();
191
+ if (!stored)
192
+ return;
193
+ try {
194
+ await this.post('/v1/deactivate', { licenseKey: stored.licenseKey, product: this.opts.product, fingerprint: await this.fingerprint() });
195
+ }
196
+ catch (e) {
197
+ if (!(e instanceof LicenseError && (e.code === 'not_activated' || e.code === 'invalid_key')))
198
+ throw e;
199
+ }
200
+ await this.clear();
201
+ }
202
+ static hasFeature(state, feature) {
203
+ return (state.status === 'valid' || state.status === 'grace') && state.features.includes(feature);
204
+ }
205
+ }
206
+ exports.LicenseKit = LicenseKit;
207
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@carchalabs/licensekit-electron",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "LicenseKit SDK for the Electron main process and Node: activation, offline verification, grace period",
5
+ "homepage": "https://licensekit.carchalabs.com/docs/",
6
+ "repository": { "type": "git", "url": "git+https://github.com/carcha-labs/licensekit-sdk.git", "directory": "packages/sdk-electron" },
7
+ "bugs": { "url": "https://github.com/carcha-labs/licensekit-sdk/issues" },
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": ["dist", "src", "!dist/**/*.test.*", "!src/**/*.test.ts"],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "prepublishOnly": "npm run build",
14
+ "test": "node --no-warnings=ExperimentalWarning --test dist/*.test.js"
15
+ },
16
+ "dependencies": {
17
+ "@carchalabs/licensekit-core": "0.1.0"
18
+ },
19
+ "engines": { "node": ">=22.13" },
20
+ "publishConfig": { "access": "public" },
21
+ "license": "MIT"
22
+ }
@@ -0,0 +1,42 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { cpus, hostname, networkInterfaces, platform } from 'node:os';
4
+ import { promisify } from 'node:util';
5
+
6
+ const run = promisify(execFile);
7
+
8
+ /** Reads a stable, OS-provided machine id. Falls back to hardware traits if unavailable. */
9
+ export async function readMachineId(): Promise<string> {
10
+ try {
11
+ const os = platform();
12
+ if (os === 'darwin') {
13
+ const { stdout } = await run('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], { timeout: 3000 });
14
+ const m = stdout.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
15
+ if (m) return `mac:${m[1]}`;
16
+ } else if (os === 'win32') {
17
+ const { stdout } = await run('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], {
18
+ timeout: 3000,
19
+ windowsHide: true,
20
+ });
21
+ const m = stdout.match(/MachineGuid\s+REG_SZ\s+([\w-]+)/i);
22
+ if (m) return `win:${m[1]}`;
23
+ } else {
24
+ for (const f of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
25
+ const id = (await readFile(f, 'utf8').catch(() => '')).trim();
26
+ if (id) return `linux:${id}`;
27
+ }
28
+ }
29
+ } catch {
30
+ /* fall through */
31
+ }
32
+ return `fallback:${fallbackTraits()}`;
33
+ }
34
+
35
+ function fallbackTraits(): string {
36
+ const macs = Object.values(networkInterfaces())
37
+ .flat()
38
+ .filter((i) => i && !i.internal && i.mac && i.mac !== '00:00:00:00:00:00')
39
+ .map((i) => i!.mac)
40
+ .sort();
41
+ return [hostname(), cpus()[0]?.model ?? '', macs[0] ?? ''].join('|');
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,254 @@
1
+ import { KeyObject } from 'node:crypto';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { hostname, platform } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { hashFingerprint, keySet, LeaseClaims, LeaseFailure, verifyLease } from '@carchalabs/licensekit-core';
6
+ import { readMachineId } from './fingerprint';
7
+
8
+ export { readMachineId };
9
+
10
+ export type LicenseStatus = 'valid' | 'grace' | 'invalid' | 'unlicensed' | 'clock_suspect';
11
+
12
+ export interface LicenseState {
13
+ status: LicenseStatus;
14
+ offline: boolean;
15
+ storageProtected: boolean;
16
+ reason?: string;
17
+ message?: string;
18
+ customer?: string;
19
+ features: string[];
20
+ expiresAt: string | null;
21
+ maintenanceUntil?: string | null;
22
+ revalidateBy?: string;
23
+ keyHint?: string;
24
+ }
25
+
26
+ export interface SecureStore {
27
+ encrypt(plain: string): Buffer | Promise<Buffer>;
28
+ decrypt(cipher: Buffer): string | Promise<string>;
29
+ isProtected?(): boolean;
30
+ }
31
+
32
+ export interface LicenseKitOptions {
33
+ serverUrl: string;
34
+ product: string;
35
+ publicKey: string | string[];
36
+ storageDir: string;
37
+ appVersion?: string;
38
+ graceDays?: number;
39
+ secureStore?: SecureStore;
40
+ requestTimeoutMs?: number;
41
+ fetchImpl?: typeof fetch;
42
+ machineId?: () => Promise<string>;
43
+ now?: () => number;
44
+ }
45
+
46
+ interface Stored {
47
+ licenseKey: string;
48
+ token: string;
49
+ lastSeen?: number;
50
+ }
51
+
52
+ const DEFINITIVE = new Set(['invalid_key', 'not_activated', 'license_suspended', 'license_revoked', 'license_expired']);
53
+ const CLOCK_TOLERANCE_SECONDS = 24 * 3600;
54
+ const DEFAULT_GRACE_DAYS = 7;
55
+ const CLOCK_MESSAGE = 'The date and time on this computer look wrong. Correct them, then connect to the internet.';
56
+
57
+ export class LicenseError extends Error {
58
+ constructor(
59
+ public readonly code: string,
60
+ message: string,
61
+ public readonly status?: number,
62
+ ) {
63
+ super(message);
64
+ this.name = 'LicenseError';
65
+ }
66
+ }
67
+
68
+ const REASON_TEXT: Record<LeaseFailure, string> = {
69
+ malformed: 'The saved license is damaged. Please activate again.',
70
+ bad_signature: 'The saved license was modified. Please activate again.',
71
+ unsupported_algorithm: 'Please update the app.',
72
+ wrong_type: 'Please update the app.',
73
+ unknown_key: 'Please update the app.',
74
+ wrong_issuer: 'This license was issued by a different license server.',
75
+ not_yet_valid: CLOCK_MESSAGE,
76
+ wrong_product: 'This license is for a different product.',
77
+ wrong_device: 'This license is activated on another device.',
78
+ expired: 'Your license has expired.',
79
+ revalidation_required: 'Connect to the internet to confirm your license.',
80
+ };
81
+
82
+ const toIso = (seconds: number) => new Date(seconds * 1000).toISOString();
83
+
84
+ export class LicenseKit {
85
+ private readonly file: string;
86
+ private readonly keys: Map<string, KeyObject>;
87
+ private fingerprintCache?: string;
88
+
89
+ constructor(private readonly opts: LicenseKitOptions) {
90
+ this.file = join(opts.storageDir, 'license.dat');
91
+ this.keys = keySet(Array.isArray(opts.publicKey) ? opts.publicKey : [opts.publicKey]);
92
+ }
93
+
94
+ private get fetch() {
95
+ return this.opts.fetchImpl ?? fetch;
96
+ }
97
+
98
+ private nowSec() {
99
+ return Math.floor((this.opts.now?.() ?? Date.now()) / 1000);
100
+ }
101
+
102
+ private get storageProtected(): boolean {
103
+ const store = this.opts.secureStore;
104
+ return store !== undefined && (store.isProtected?.() ?? true);
105
+ }
106
+
107
+ async fingerprint(): Promise<string> {
108
+ if (!this.fingerprintCache) {
109
+ const raw = await (this.opts.machineId ?? readMachineId)();
110
+ this.fingerprintCache = hashFingerprint(raw, this.opts.product);
111
+ }
112
+ return this.fingerprintCache;
113
+ }
114
+
115
+ private async load(): Promise<Stored | null> {
116
+ try {
117
+ const buf = await readFile(this.file);
118
+ const text = this.opts.secureStore ? await this.opts.secureStore.decrypt(buf) : buf.toString('utf8');
119
+ return JSON.parse(text);
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ private async save(stored: Stored) {
126
+ await mkdir(this.opts.storageDir, { recursive: true });
127
+ const text = JSON.stringify(stored);
128
+ await writeFile(this.file, this.opts.secureStore ? await this.opts.secureStore.encrypt(text) : text, { mode: 0o600 });
129
+ }
130
+
131
+ private async clear() {
132
+ await rm(this.file, { force: true });
133
+ }
134
+
135
+ private async post(path: string, body: Record<string, unknown>): Promise<any> {
136
+ let res: Response;
137
+ try {
138
+ res = await this.fetch(this.opts.serverUrl.replace(/\/+$/, '') + path, {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify(body),
142
+ signal: AbortSignal.timeout(this.opts.requestTimeoutMs ?? 8000),
143
+ });
144
+ } catch {
145
+ throw new LicenseError('network', 'Could not reach the license server.');
146
+ }
147
+ const json: any = await res.json().catch(() => ({}));
148
+ if (!res.ok) throw new LicenseError(json.error ?? 'server_error', json.message ?? `License server error (${res.status})`, res.status);
149
+ return json;
150
+ }
151
+
152
+ private emptyState(status: LicenseStatus, offline: boolean, extra: Partial<LicenseState> = {}): LicenseState {
153
+ return { status, offline, storageProtected: this.storageProtected, features: [], expiresAt: null, ...extra };
154
+ }
155
+
156
+ private stateFromLease(stored: Stored, offline: boolean): LicenseState {
157
+ const fingerprintHash = this.fingerprintCache!;
158
+ const now = this.nowSec();
159
+ const keyHint = `${stored.licenseKey.slice(0, 7)}…${stored.licenseKey.slice(-4)}`;
160
+ if (offline && stored.lastSeen !== undefined && now < stored.lastSeen - CLOCK_TOLERANCE_SECONDS) {
161
+ return this.emptyState('clock_suspect', offline, { reason: 'clock_rollback', message: CLOCK_MESSAGE, keyHint });
162
+ }
163
+ const verifyOptions = { keys: this.keys, product: this.opts.product, fingerprintHash, now };
164
+ const strict = verifyLease(stored.token, verifyOptions);
165
+ const fromClaims = (claims: LeaseClaims) => ({
166
+ customer: claims.cus,
167
+ features: claims.feat,
168
+ expiresAt: claims.exp === undefined ? null : toIso(claims.exp),
169
+ maintenanceUntil: claims.mnt === undefined ? null : toIso(claims.mnt),
170
+ storageProtected: this.storageProtected,
171
+ keyHint,
172
+ });
173
+ if (strict.ok) {
174
+ return { status: 'valid', offline, ...fromClaims(strict.claims), revalidateBy: toIso(strict.claims.rev) };
175
+ }
176
+ if (strict.reason === 'not_yet_valid') {
177
+ return this.emptyState('clock_suspect', offline, { reason: 'lease_from_future', message: CLOCK_MESSAGE, keyHint });
178
+ }
179
+ if (strict.reason === 'revalidation_required' && strict.claims) {
180
+ const grace = strict.claims.grc ?? (this.opts.graceDays ?? DEFAULT_GRACE_DAYS) * 86400;
181
+ const lenient = verifyLease(stored.token, { ...verifyOptions, graceSeconds: grace });
182
+ if (lenient.ok) {
183
+ return {
184
+ status: 'grace',
185
+ offline,
186
+ reason: 'offline_grace',
187
+ message: 'Working offline. Connect to the internet soon to keep using the app.',
188
+ ...fromClaims(lenient.claims),
189
+ revalidateBy: toIso(Math.min(lenient.claims.rev + grace, lenient.claims.exp ?? Infinity)),
190
+ };
191
+ }
192
+ }
193
+ return this.emptyState('invalid', offline, { reason: strict.reason, message: REASON_TEXT[strict.reason], keyHint });
194
+ }
195
+
196
+ async activate(licenseKey: string, machineName = hostname()): Promise<LicenseState> {
197
+ const fingerprint = await this.fingerprint();
198
+ const key = licenseKey.trim().toUpperCase();
199
+ const response = await this.post('/v1/activate', {
200
+ licenseKey: key,
201
+ product: this.opts.product,
202
+ fingerprint,
203
+ machineName: machineName.slice(0, 80),
204
+ platform: platform(),
205
+ appVersion: this.opts.appVersion,
206
+ });
207
+ const stored: Stored = { licenseKey: key, token: response.token, lastSeen: this.nowSec() };
208
+ const state = this.stateFromLease(stored, false);
209
+ if (state.status !== 'valid') throw new LicenseError(state.status === 'clock_suspect' ? 'clock_suspect' : state.reason ?? 'invalid', state.message ?? 'Server returned an unusable license');
210
+ await this.save(stored);
211
+ return state;
212
+ }
213
+
214
+ async check(): Promise<LicenseState> {
215
+ await this.fingerprint();
216
+ const stored = await this.load();
217
+ if (!stored) return this.emptyState('unlicensed', false);
218
+ try {
219
+ const response = await this.post('/v1/validate', {
220
+ licenseKey: stored.licenseKey,
221
+ product: this.opts.product,
222
+ fingerprint: this.fingerprintCache,
223
+ appVersion: this.opts.appVersion,
224
+ });
225
+ const fresh: Stored = { ...stored, token: response.token, lastSeen: this.nowSec() };
226
+ await this.save(fresh);
227
+ return this.stateFromLease(fresh, false);
228
+ } catch (e) {
229
+ if (e instanceof LicenseError && DEFINITIVE.has(e.code)) {
230
+ return this.emptyState('invalid', false, { reason: e.code, message: e.message });
231
+ }
232
+ const state = this.stateFromLease(stored, true);
233
+ if (state.status === 'valid' || state.status === 'grace') {
234
+ await this.save({ ...stored, lastSeen: Math.max(stored.lastSeen ?? 0, this.nowSec()) });
235
+ }
236
+ return state;
237
+ }
238
+ }
239
+
240
+ async deactivate(): Promise<void> {
241
+ const stored = await this.load();
242
+ if (!stored) return;
243
+ try {
244
+ await this.post('/v1/deactivate', { licenseKey: stored.licenseKey, product: this.opts.product, fingerprint: await this.fingerprint() });
245
+ } catch (e) {
246
+ if (!(e instanceof LicenseError && (e.code === 'not_activated' || e.code === 'invalid_key'))) throw e;
247
+ }
248
+ await this.clear();
249
+ }
250
+
251
+ static hasFeature(state: LicenseState, feature: string): boolean {
252
+ return (state.status === 'valid' || state.status === 'grace') && state.features.includes(feature);
253
+ }
254
+ }